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}