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}