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//! - [`HandlerError`] / [`HandlerResult`] - Handler processing failures (mapped to `genErr`)
10//! - [`Response`] - Internal response type (typically not used directly)
11
12use std::borrow::Cow;
13
14use crate::error::ErrorStatus;
15use crate::value::Value;
16use crate::varbind::VarBind;
17
18/// A handler processing failure, reported to the manager as a `genErr` Response.
19///
20/// Returned as the `Err` side of [`HandlerResult`] from
21/// [`MibHandler::get`](super::MibHandler::get) and
22/// [`MibHandler::get_next`](super::MibHandler::get_next) when the handler
23/// *failed to process* the request — the backing store was unreachable, a lock
24/// was poisoned, a hardware read timed out. It is distinct from the RFC 3416
25/// exception values, which are successful answers about the MIB:
26///
27/// | Situation | Return |
28/// |-----------|--------|
29/// | Object/instance doesn't exist | `Ok(GetResult::NoSuchObject / NoSuchInstance)` |
30/// | No more OIDs in the subtree | `Ok(GetNextResult::EndOfMibView)` |
31/// | Couldn't find out (backend failure) | `Err(HandlerError)` |
32///
33/// When a handler returns an error, the agent answers the whole request with
34/// error-status `genErr` and error-index set to the failing variable binding
35/// (RFC 3416 Section 4.2.1), for all protocol versions. The message and source
36/// are logged by the agent; they are never sent on the wire — `genErr` carries
37/// no detail.
38///
39/// # Construction
40///
41/// Any [`std::error::Error`] converts via `?`, or build one from a message:
42///
43/// ```rust
44/// use async_snmp::handler::{HandlerError, HandlerResult, GetResult};
45///
46/// fn read_backend() -> std::io::Result<i32> {
47///     Err(std::io::Error::other("device bus timeout"))
48/// }
49///
50/// fn get_value() -> HandlerResult<GetResult> {
51///     let raw = read_backend()?; // io::Error -> HandlerError
52///     Ok(GetResult::Value(async_snmp::Value::Integer(raw)))
53/// }
54///
55/// let err: HandlerError = HandlerError::new("cache poisoned");
56/// assert_eq!(err.message(), "cache poisoned");
57/// assert!(get_value().is_err());
58/// ```
59///
60/// `HandlerError` intentionally does not implement [`std::error::Error`]:
61/// that keeps the blanket `From<E: std::error::Error>` conversion (and with it
62/// `?` on arbitrary error types) possible, the same trade-off `anyhow::Error`
63/// makes. It is a terminal type — the agent consumes it; nothing converts out
64/// of it.
65pub struct HandlerError {
66    message: Cow<'static, str>,
67    source: Option<Box<dyn std::error::Error + Send + Sync>>,
68}
69
70impl HandlerError {
71    /// Create an error from a message describing what failed.
72    pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
73        Self {
74            message: message.into(),
75            source: None,
76        }
77    }
78
79    /// The failure message (used for agent-side logging only; never sent on
80    /// the wire).
81    #[must_use]
82    pub fn message(&self) -> &str {
83        &self.message
84    }
85
86    /// The underlying error this was converted from, if any.
87    #[must_use]
88    pub fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
89        self.source.as_deref().map(|e| e as _)
90    }
91}
92
93impl std::fmt::Display for HandlerError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "{}", self.message)
96    }
97}
98
99impl std::fmt::Debug for HandlerError {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.debug_struct("HandlerError")
102            .field("message", &self.message)
103            .field("source", &self.source)
104            .finish()
105    }
106}
107
108impl<E: std::error::Error + Send + Sync + 'static> From<E> for HandlerError {
109    fn from(err: E) -> Self {
110        Self {
111            message: err.to_string().into(),
112            source: Some(Box::new(err)),
113        }
114    }
115}
116
117/// Result type returned by [`MibHandler::get`](super::MibHandler::get) and
118/// [`MibHandler::get_next`](super::MibHandler::get_next).
119///
120/// `Err` means the handler failed to process the request and the agent must
121/// answer `genErr`; see [`HandlerError`] for when to return which.
122pub type HandlerResult<T> = Result<T, HandlerError>;
123
124/// Result of a SET operation phase (RFC 3416).
125///
126/// This enum is used by the multi-phase SET protocol:
127/// - [`MibHandler::test_set`](super::MibHandler::test_set): Returns `Ok` if the SET would succeed
128/// - [`MibHandler::commit_set`](super::MibHandler::commit_set): Returns `Ok` if the change was applied
129/// - [`MibHandler::undo_set`](super::MibHandler::undo_set): Returns `Ok` on successful rollback
130/// - [`MibHandler::free_set`](super::MibHandler::free_set): Returns `()` (cleanup is best-effort)
131///
132/// # Choosing the Right Error
133///
134/// | Situation | Variant |
135/// |-----------|---------|
136/// | SET succeeded | [`Ok`](SetResult::Ok) |
137/// | User lacks permission | [`NoAccess`](SetResult::NoAccess) |
138/// | Object is read-only by design | [`NotWritable`](SetResult::NotWritable) |
139/// | Wrong ASN.1 type (e.g., String for Integer) | [`WrongType`](SetResult::WrongType) |
140/// | Value too long/short | [`WrongLength`](SetResult::WrongLength) |
141/// | Value encoding error | [`WrongEncoding`](SetResult::WrongEncoding) |
142/// | Semantic validation failed | [`WrongValue`](SetResult::WrongValue) |
143/// | Cannot create table row | [`NoCreation`](SetResult::NoCreation) |
144/// | Values conflict within request | [`InconsistentValue`](SetResult::InconsistentValue) |
145/// | Out of memory, lock contention | [`ResourceUnavailable`](SetResult::ResourceUnavailable) |
146///
147/// # Example
148///
149/// ```rust
150/// use async_snmp::handler::SetResult;
151/// use async_snmp::Value;
152///
153/// fn validate_admin_status(value: &Value) -> SetResult {
154///     match value {
155///         Value::Integer(v) if *v == 1 || *v == 2 => SetResult::Ok, // up(1) or down(2)
156///         Value::Integer(_) => SetResult::WrongValue, // Invalid admin status
157///         _ => SetResult::WrongType, // Must be Integer
158///     }
159/// }
160///
161/// assert_eq!(validate_admin_status(&Value::Integer(1)), SetResult::Ok);
162/// assert_eq!(validate_admin_status(&Value::Integer(99)), SetResult::WrongValue);
163/// assert_eq!(validate_admin_status(&Value::OctetString("up".into())), SetResult::WrongType);
164/// ```
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum SetResult {
167    /// Operation succeeded.
168    Ok,
169    /// Access denied (security/authorization failure).
170    ///
171    /// Use this when the request lacks sufficient access rights to modify
172    /// the object, based on the security context (user, community, etc.).
173    /// Maps to RFC 3416 error status code 6 (noAccess).
174    NoAccess,
175    /// Object is inherently read-only (not writable by design).
176    ///
177    /// Use this when the object cannot be modified regardless of who
178    /// is making the request. Maps to RFC 3416 error status code 17 (notWritable).
179    NotWritable,
180    /// Value has wrong ASN.1 type for this OID.
181    ///
182    /// Use when the provided value type doesn't match the expected type
183    /// (e.g., `OctetString` provided for an Integer object).
184    WrongType,
185    /// Value has wrong length for this OID.
186    ///
187    /// Use when the value length violates constraints (e.g., `DisplayString`
188    /// longer than 255 characters).
189    WrongLength,
190    /// Value encoding is incorrect.
191    WrongEncoding,
192    /// Value is not valid for this OID (semantic check failed).
193    ///
194    /// Use when the value type is correct but the value itself is invalid
195    /// (e.g., negative value for an unsigned counter, or value outside
196    /// an enumeration's valid range).
197    WrongValue,
198    /// Cannot create new row (table doesn't support row creation).
199    NoCreation,
200    /// Value is inconsistent with other values in the same SET.
201    InconsistentValue,
202    /// Resource unavailable (memory, locks, etc.).
203    ResourceUnavailable,
204    /// Commit failed (internal error during apply).
205    CommitFailed,
206    /// Undo failed (internal error during rollback).
207    UndoFailed,
208    /// Row name is inconsistent with existing data.
209    InconsistentName,
210}
211
212impl SetResult {
213    /// Check if this result indicates success.
214    #[must_use]
215    pub fn is_ok(&self) -> bool {
216        matches!(self, SetResult::Ok)
217    }
218
219    /// Convert to an `ErrorStatus` code.
220    #[must_use]
221    pub fn to_error_status(&self) -> ErrorStatus {
222        match self {
223            SetResult::Ok => ErrorStatus::NoError,
224            SetResult::NoAccess => ErrorStatus::NoAccess,
225            SetResult::NotWritable => ErrorStatus::NotWritable,
226            SetResult::WrongType => ErrorStatus::WrongType,
227            SetResult::WrongLength => ErrorStatus::WrongLength,
228            SetResult::WrongEncoding => ErrorStatus::WrongEncoding,
229            SetResult::WrongValue => ErrorStatus::WrongValue,
230            SetResult::NoCreation => ErrorStatus::NoCreation,
231            SetResult::InconsistentValue => ErrorStatus::InconsistentValue,
232            SetResult::ResourceUnavailable => ErrorStatus::ResourceUnavailable,
233            SetResult::CommitFailed => ErrorStatus::CommitFailed,
234            SetResult::UndoFailed => ErrorStatus::UndoFailed,
235            SetResult::InconsistentName => ErrorStatus::InconsistentName,
236        }
237    }
238}
239
240/// Response to return from a handler.
241///
242/// This is typically built internally by the agent based on handler results.
243#[derive(Debug, Clone)]
244pub struct Response {
245    /// Variable bindings in the response
246    pub varbinds: Vec<VarBind>,
247    /// Error status (0 = no error)
248    pub error_status: ErrorStatus,
249    /// Error index (1-based index of problematic varbind, 0 if no error)
250    pub error_index: i32,
251}
252
253impl Response {
254    /// Create a successful response with the given varbinds.
255    #[must_use]
256    pub fn success(varbinds: Vec<VarBind>) -> Self {
257        Self {
258            varbinds,
259            error_status: ErrorStatus::NoError,
260            error_index: 0,
261        }
262    }
263
264    /// Create an error response.
265    #[must_use]
266    pub fn error(error_status: ErrorStatus, error_index: i32, varbinds: Vec<VarBind>) -> Self {
267        Self {
268            varbinds,
269            error_status,
270            error_index,
271        }
272    }
273}
274
275/// Result of a GET operation on a specific OID (RFC 3416).
276///
277/// This enum distinguishes between the RFC 3416-mandated exception types:
278/// - `Value`: The OID exists and has the given value
279/// - `NoSuchObject`: The OID's object type is not supported (agent doesn't implement this MIB)
280/// - `NoSuchInstance`: The object type exists but this specific instance doesn't
281///   (e.g., table row doesn't exist)
282///
283/// # Version Differences
284///
285/// - **`SNMPv1`**: Both exception types result in a `noSuchName` error response
286/// - **SNMPv2c/v3**: Returns the appropriate exception value in the response varbind
287///
288/// # Choosing `NoSuchObject` vs `NoSuchInstance`
289///
290/// | Situation | Variant |
291/// |-----------|---------|
292/// | OID prefix not recognized | [`NoSuchObject`](GetResult::NoSuchObject) |
293/// | Scalar object not implemented | [`NoSuchObject`](GetResult::NoSuchObject) |
294/// | Table column not implemented | [`NoSuchObject`](GetResult::NoSuchObject) |
295/// | Table row doesn't exist | [`NoSuchInstance`](GetResult::NoSuchInstance) |
296/// | Scalar has no value (optional) | [`NoSuchInstance`](GetResult::NoSuchInstance) |
297///
298/// # Example: Scalar Objects
299///
300/// ```rust
301/// use async_snmp::handler::GetResult;
302/// use async_snmp::{Value, oid};
303///
304/// fn get_scalar(oid: &async_snmp::Oid) -> GetResult {
305///     if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {  // sysDescr.0
306///         GetResult::Value(Value::OctetString("My SNMP Agent".into()))
307///     } else if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 2, 0) {  // sysObjectID.0
308///         GetResult::Value(Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999)))
309///     } else {
310///         GetResult::NoSuchObject
311///     }
312/// }
313/// ```
314///
315/// # Example: Table Objects
316///
317/// ```rust
318/// use async_snmp::handler::GetResult;
319/// use async_snmp::{Value, Oid, oid};
320///
321/// struct IfTable {
322///     entries: Vec<(u32, String)>,  // (index, description)
323/// }
324///
325/// impl IfTable {
326///     fn get(&self, oid: &Oid) -> GetResult {
327///         let if_descr_prefix = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2);
328///
329///         if !oid.starts_with(&if_descr_prefix) {
330///             return GetResult::NoSuchObject;  // Not our column
331///         }
332///
333///         // Extract index from OID (position after prefix)
334///         let arcs = oid.arcs();
335///         if arcs.len() != if_descr_prefix.len() + 1 {
336///             return GetResult::NoSuchInstance;  // Wrong index format
337///         }
338///
339///         let index = arcs[if_descr_prefix.len()];
340///         match self.entries.iter().find(|(i, _)| *i == index) {
341///             Some((_, desc)) => GetResult::Value(Value::OctetString(desc.clone().into())),
342///             None => GetResult::NoSuchInstance,  // Row doesn't exist
343///         }
344///     }
345/// }
346/// ```
347#[derive(Debug, Clone, PartialEq)]
348pub enum GetResult {
349    /// The OID exists and has this value.
350    Value(Value),
351    /// The object type is not implemented by this agent.
352    ///
353    /// Use this when the OID prefix (object type) is not recognized.
354    /// This typically means the handler doesn't implement this part of the MIB.
355    NoSuchObject,
356    /// The object type exists but this specific instance doesn't.
357    ///
358    /// Use this when the OID prefix is valid but the instance identifier
359    /// (e.g., table index) doesn't exist. This is common for table objects
360    /// where the row has been deleted or never existed.
361    NoSuchInstance,
362}
363
364impl GetResult {
365    /// Create a `GetResult` from an `Option<Value>`.
366    ///
367    /// This is a convenience method for migrating from the previous
368    /// `Option<Value>` interface. `None` is treated as `NoSuchObject`.
369    pub fn from_option(value: Option<Value>) -> Self {
370        match value {
371            Some(v) => GetResult::Value(v),
372            None => GetResult::NoSuchObject,
373        }
374    }
375}
376
377impl From<Value> for GetResult {
378    fn from(value: Value) -> Self {
379        GetResult::Value(value)
380    }
381}
382
383impl From<Option<Value>> for GetResult {
384    fn from(value: Option<Value>) -> Self {
385        GetResult::from_option(value)
386    }
387}
388
389/// Result of a GETNEXT operation (RFC 3416).
390///
391/// GETNEXT retrieves the lexicographically next OID after the requested one.
392/// This is the foundation of SNMP walking (iterating through MIB subtrees)
393/// and is also used internally by GETBULK.
394///
395/// # Version Differences
396///
397/// - **`SNMPv1`**: `EndOfMibView` results in a `noSuchName` error response
398/// - **SNMPv2c/v3**: Returns the `endOfMibView` exception value in the response
399///
400/// # Lexicographic Ordering
401///
402/// OIDs are compared arc-by-arc as unsigned integers:
403/// - `1.3.6.1.2` < `1.3.6.1.2.1` (shorter is less than longer with same prefix)
404/// - `1.3.6.1.2.1` < `1.3.6.1.3` (compare at first differing arc)
405/// - `1.3.6.1.10` > `1.3.6.1.9` (numeric comparison, not lexicographic string)
406///
407/// # Example
408///
409/// ```rust
410/// use async_snmp::handler::GetNextResult;
411/// use async_snmp::{Value, VarBind, Oid, oid};
412///
413/// struct SimpleTable {
414///     oids: Vec<(Oid, Value)>,  // Must be sorted!
415/// }
416///
417/// impl SimpleTable {
418///     fn get_next(&self, after: &Oid) -> GetNextResult {
419///         // Find first OID that is strictly greater than 'after'
420///         for (oid, value) in &self.oids {
421///             if oid > after {
422///                 return GetNextResult::Value(VarBind::new(oid.clone(), value.clone()));
423///             }
424///         }
425///         GetNextResult::EndOfMibView
426///     }
427/// }
428///
429/// let table = SimpleTable {
430///     oids: vec![
431///         (oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::OctetString("sysDescr".into())),
432///         (oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::TimeTicks(12345)),
433///     ],
434/// };
435///
436/// // Before first OID - returns first
437/// let result = table.get_next(&oid!(1, 3, 6, 1, 2, 1, 1, 0));
438/// assert!(result.is_value());
439///
440/// // After last OID - returns EndOfMibView
441/// let result = table.get_next(&oid!(1, 3, 6, 1, 2, 1, 1, 3, 0));
442/// assert!(result.is_end_of_mib_view());
443/// ```
444#[derive(Debug, Clone, PartialEq)]
445pub enum GetNextResult {
446    /// The next OID/value pair in the MIB tree.
447    ///
448    /// The returned OID must be strictly greater than the input OID.
449    Value(VarBind),
450    /// No more OIDs after the given one (end of MIB view).
451    ///
452    /// Return this when the requested OID is at or past the last OID
453    /// in your handler's subtree.
454    EndOfMibView,
455}
456
457impl GetNextResult {
458    /// Create a `GetNextResult` from an `Option<VarBind>`.
459    ///
460    /// This is a convenience method for migrating from the previous
461    /// `Option<VarBind>` interface. `None` is treated as `EndOfMibView`.
462    pub fn from_option(value: Option<VarBind>) -> Self {
463        match value {
464            Some(vb) => GetNextResult::Value(vb),
465            None => GetNextResult::EndOfMibView,
466        }
467    }
468
469    /// Returns `true` if this is a value result.
470    pub fn is_value(&self) -> bool {
471        matches!(self, GetNextResult::Value(_))
472    }
473
474    /// Returns `true` if this is end of MIB view.
475    pub fn is_end_of_mib_view(&self) -> bool {
476        matches!(self, GetNextResult::EndOfMibView)
477    }
478
479    /// Converts to an `Option<VarBind>`.
480    pub fn into_option(self) -> Option<VarBind> {
481        match self {
482            GetNextResult::Value(vb) => Some(vb),
483            GetNextResult::EndOfMibView => None,
484        }
485    }
486}
487
488impl From<VarBind> for GetNextResult {
489    fn from(vb: VarBind) -> Self {
490        GetNextResult::Value(vb)
491    }
492}
493
494impl From<Option<VarBind>> for GetNextResult {
495    fn from(value: Option<VarBind>) -> Self {
496        GetNextResult::from_option(value)
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use crate::oid;
504
505    #[test]
506    fn test_response_success() {
507        let response = Response::success(vec![VarBind::new(oid!(1, 3, 6, 1), Value::Integer(1))]);
508        assert_eq!(response.error_status, ErrorStatus::NoError);
509        assert_eq!(response.error_index, 0);
510        assert_eq!(response.varbinds.len(), 1);
511    }
512
513    #[test]
514    fn test_response_error() {
515        let response = Response::error(
516            ErrorStatus::NoSuchName,
517            1,
518            vec![VarBind::new(oid!(1, 3, 6, 1), Value::Null)],
519        );
520        assert_eq!(response.error_status, ErrorStatus::NoSuchName);
521        assert_eq!(response.error_index, 1);
522    }
523
524    #[test]
525    fn test_get_result_from_option() {
526        let result = GetResult::from_option(Some(Value::Integer(42)));
527        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
528
529        let result = GetResult::from_option(None);
530        assert!(matches!(result, GetResult::NoSuchObject));
531    }
532
533    #[test]
534    fn test_get_result_from_value() {
535        let result: GetResult = Value::Integer(42).into();
536        assert!(matches!(result, GetResult::Value(Value::Integer(42))));
537    }
538
539    #[test]
540    fn test_get_next_result_from_option() {
541        let vb = VarBind::new(oid!(1, 3, 6, 1), Value::Integer(42));
542        let result = GetNextResult::from_option(Some(vb.clone()));
543        assert!(result.is_value());
544        assert_eq!(result.into_option(), Some(vb));
545
546        let result = GetNextResult::from_option(None);
547        assert!(result.is_end_of_mib_view());
548        assert_eq!(result.into_option(), None);
549    }
550
551    #[test]
552    fn test_get_next_result_from_varbind() {
553        let vb = VarBind::new(oid!(1, 3, 6, 1), Value::Integer(42));
554        let result: GetNextResult = vb.clone().into();
555        assert!(result.is_value());
556        if let GetNextResult::Value(inner) = result {
557            assert_eq!(inner.oid, oid!(1, 3, 6, 1));
558        }
559    }
560
561    #[test]
562    fn test_set_result_to_error_status() {
563        assert_eq!(SetResult::Ok.to_error_status(), ErrorStatus::NoError);
564        assert_eq!(SetResult::NoAccess.to_error_status(), ErrorStatus::NoAccess);
565        assert_eq!(
566            SetResult::NotWritable.to_error_status(),
567            ErrorStatus::NotWritable
568        );
569        assert_eq!(
570            SetResult::WrongType.to_error_status(),
571            ErrorStatus::WrongType
572        );
573        assert_eq!(
574            SetResult::CommitFailed.to_error_status(),
575            ErrorStatus::CommitFailed
576        );
577    }
578
579    #[test]
580    fn test_handler_error_message_only() {
581        let err = HandlerError::new("backend down");
582        assert_eq!(err.message(), "backend down");
583        assert!(err.source().is_none());
584        assert_eq!(err.to_string(), "backend down");
585    }
586
587    #[test]
588    fn test_handler_error_from_std_error() {
589        let io_err = std::io::Error::other("bus timeout");
590        let err: HandlerError = io_err.into();
591        assert_eq!(err.message(), "bus timeout");
592        assert!(err.source().is_some());
593        assert_eq!(err.to_string(), "bus timeout");
594    }
595
596    #[test]
597    fn test_handler_error_question_mark_conversion() {
598        fn inner() -> HandlerResult<GetResult> {
599            Err(std::io::Error::other("no route"))?;
600            unreachable!()
601        }
602        let err = inner().unwrap_err();
603        assert_eq!(err.message(), "no route");
604    }
605
606    #[test]
607    fn test_set_result_is_ok() {
608        assert!(SetResult::Ok.is_ok());
609        assert!(!SetResult::NoAccess.is_ok());
610        assert!(!SetResult::NotWritable.is_ok());
611    }
612}