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}