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 #[must_use]
106 pub fn is_ok(&self) -> bool {
107 matches!(self, SetResult::Ok)
108 }
109
110 /// Convert to an `ErrorStatus` code.
111 #[must_use]
112 pub fn to_error_status(&self) -> ErrorStatus {
113 match self {
114 SetResult::Ok => ErrorStatus::NoError,
115 SetResult::NoAccess => ErrorStatus::NoAccess,
116 SetResult::NotWritable => ErrorStatus::NotWritable,
117 SetResult::WrongType => ErrorStatus::WrongType,
118 SetResult::WrongLength => ErrorStatus::WrongLength,
119 SetResult::WrongEncoding => ErrorStatus::WrongEncoding,
120 SetResult::WrongValue => ErrorStatus::WrongValue,
121 SetResult::NoCreation => ErrorStatus::NoCreation,
122 SetResult::InconsistentValue => ErrorStatus::InconsistentValue,
123 SetResult::ResourceUnavailable => ErrorStatus::ResourceUnavailable,
124 SetResult::CommitFailed => ErrorStatus::CommitFailed,
125 SetResult::UndoFailed => ErrorStatus::UndoFailed,
126 SetResult::InconsistentName => ErrorStatus::InconsistentName,
127 }
128 }
129}
130
131/// Response to return from a handler.
132///
133/// This is typically built internally by the agent based on handler results.
134#[derive(Debug, Clone)]
135pub struct Response {
136 /// Variable bindings in the response
137 pub varbinds: Vec<VarBind>,
138 /// Error status (0 = no error)
139 pub error_status: ErrorStatus,
140 /// Error index (1-based index of problematic varbind, 0 if no error)
141 pub error_index: i32,
142}
143
144impl Response {
145 /// Create a successful response with the given varbinds.
146 #[must_use]
147 pub fn success(varbinds: Vec<VarBind>) -> Self {
148 Self {
149 varbinds,
150 error_status: ErrorStatus::NoError,
151 error_index: 0,
152 }
153 }
154
155 /// Create an error response.
156 #[must_use]
157 pub fn error(error_status: ErrorStatus, error_index: i32, varbinds: Vec<VarBind>) -> Self {
158 Self {
159 varbinds,
160 error_status,
161 error_index,
162 }
163 }
164}
165
166/// Result of a GET operation on a specific OID (RFC 3416).
167///
168/// This enum distinguishes between the RFC 3416-mandated exception types:
169/// - `Value`: The OID exists and has the given value
170/// - `NoSuchObject`: The OID's object type is not supported (agent doesn't implement this MIB)
171/// - `NoSuchInstance`: The object type exists but this specific instance doesn't
172/// (e.g., table row doesn't exist)
173///
174/// # Version Differences
175///
176/// - **`SNMPv1`**: Both exception types result in a `noSuchName` error response
177/// - **SNMPv2c/v3**: Returns the appropriate exception value in the response varbind
178///
179/// # Choosing `NoSuchObject` vs `NoSuchInstance`
180///
181/// | Situation | Variant |
182/// |-----------|---------|
183/// | OID prefix not recognized | [`NoSuchObject`](GetResult::NoSuchObject) |
184/// | Scalar object not implemented | [`NoSuchObject`](GetResult::NoSuchObject) |
185/// | Table column not implemented | [`NoSuchObject`](GetResult::NoSuchObject) |
186/// | Table row doesn't exist | [`NoSuchInstance`](GetResult::NoSuchInstance) |
187/// | Scalar has no value (optional) | [`NoSuchInstance`](GetResult::NoSuchInstance) |
188///
189/// # Example: Scalar Objects
190///
191/// ```rust
192/// use async_snmp::handler::GetResult;
193/// use async_snmp::{Value, oid};
194///
195/// fn get_scalar(oid: &async_snmp::Oid) -> GetResult {
196/// if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) { // sysDescr.0
197/// GetResult::Value(Value::OctetString("My SNMP Agent".into()))
198/// } else if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 2, 0) { // sysObjectID.0
199/// GetResult::Value(Value::ObjectIdentifier(oid!(1, 3, 6, 1, 4, 1, 99999)))
200/// } else {
201/// GetResult::NoSuchObject
202/// }
203/// }
204/// ```
205///
206/// # Example: Table Objects
207///
208/// ```rust
209/// use async_snmp::handler::GetResult;
210/// use async_snmp::{Value, Oid, oid};
211///
212/// struct IfTable {
213/// entries: Vec<(u32, String)>, // (index, description)
214/// }
215///
216/// impl IfTable {
217/// fn get(&self, oid: &Oid) -> GetResult {
218/// let if_descr_prefix = oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2);
219///
220/// if !oid.starts_with(&if_descr_prefix) {
221/// return GetResult::NoSuchObject; // Not our column
222/// }
223///
224/// // Extract index from OID (position after prefix)
225/// let arcs = oid.arcs();
226/// if arcs.len() != if_descr_prefix.len() + 1 {
227/// return GetResult::NoSuchInstance; // Wrong index format
228/// }
229///
230/// let index = arcs[if_descr_prefix.len()];
231/// match self.entries.iter().find(|(i, _)| *i == index) {
232/// Some((_, desc)) => GetResult::Value(Value::OctetString(desc.clone().into())),
233/// None => GetResult::NoSuchInstance, // Row doesn't exist
234/// }
235/// }
236/// }
237/// ```
238#[derive(Debug, Clone, PartialEq)]
239pub enum GetResult {
240 /// The OID exists and has this value.
241 Value(Value),
242 /// The object type is not implemented by this agent.
243 ///
244 /// Use this when the OID prefix (object type) is not recognized.
245 /// This typically means the handler doesn't implement this part of the MIB.
246 NoSuchObject,
247 /// The object type exists but this specific instance doesn't.
248 ///
249 /// Use this when the OID prefix is valid but the instance identifier
250 /// (e.g., table index) doesn't exist. This is common for table objects
251 /// where the row has been deleted or never existed.
252 NoSuchInstance,
253}
254
255impl GetResult {
256 /// Create a `GetResult` from an `Option<Value>`.
257 ///
258 /// This is a convenience method for migrating from the previous
259 /// `Option<Value>` interface. `None` is treated as `NoSuchObject`.
260 pub fn from_option(value: Option<Value>) -> Self {
261 match value {
262 Some(v) => GetResult::Value(v),
263 None => GetResult::NoSuchObject,
264 }
265 }
266}
267
268impl From<Value> for GetResult {
269 fn from(value: Value) -> Self {
270 GetResult::Value(value)
271 }
272}
273
274impl From<Option<Value>> for GetResult {
275 fn from(value: Option<Value>) -> Self {
276 GetResult::from_option(value)
277 }
278}
279
280/// Result of a GETNEXT operation (RFC 3416).
281///
282/// GETNEXT retrieves the lexicographically next OID after the requested one.
283/// This is the foundation of SNMP walking (iterating through MIB subtrees)
284/// and is also used internally by GETBULK.
285///
286/// # Version Differences
287///
288/// - **`SNMPv1`**: `EndOfMibView` results in a `noSuchName` error response
289/// - **SNMPv2c/v3**: Returns the `endOfMibView` exception value in the response
290///
291/// # Lexicographic Ordering
292///
293/// OIDs are compared arc-by-arc as unsigned integers:
294/// - `1.3.6.1.2` < `1.3.6.1.2.1` (shorter is less than longer with same prefix)
295/// - `1.3.6.1.2.1` < `1.3.6.1.3` (compare at first differing arc)
296/// - `1.3.6.1.10` > `1.3.6.1.9` (numeric comparison, not lexicographic string)
297///
298/// # Example
299///
300/// ```rust
301/// use async_snmp::handler::GetNextResult;
302/// use async_snmp::{Value, VarBind, Oid, oid};
303///
304/// struct SimpleTable {
305/// oids: Vec<(Oid, Value)>, // Must be sorted!
306/// }
307///
308/// impl SimpleTable {
309/// fn get_next(&self, after: &Oid) -> GetNextResult {
310/// // Find first OID that is strictly greater than 'after'
311/// for (oid, value) in &self.oids {
312/// if oid > after {
313/// return GetNextResult::Value(VarBind::new(oid.clone(), value.clone()));
314/// }
315/// }
316/// GetNextResult::EndOfMibView
317/// }
318/// }
319///
320/// let table = SimpleTable {
321/// oids: vec![
322/// (oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::OctetString("sysDescr".into())),
323/// (oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::TimeTicks(12345)),
324/// ],
325/// };
326///
327/// // Before first OID - returns first
328/// let result = table.get_next(&oid!(1, 3, 6, 1, 2, 1, 1, 0));
329/// assert!(result.is_value());
330///
331/// // After last OID - returns EndOfMibView
332/// let result = table.get_next(&oid!(1, 3, 6, 1, 2, 1, 1, 3, 0));
333/// assert!(result.is_end_of_mib_view());
334/// ```
335#[derive(Debug, Clone, PartialEq)]
336pub enum GetNextResult {
337 /// The next OID/value pair in the MIB tree.
338 ///
339 /// The returned OID must be strictly greater than the input OID.
340 Value(VarBind),
341 /// No more OIDs after the given one (end of MIB view).
342 ///
343 /// Return this when the requested OID is at or past the last OID
344 /// in your handler's subtree.
345 EndOfMibView,
346}
347
348impl GetNextResult {
349 /// Create a `GetNextResult` from an `Option<VarBind>`.
350 ///
351 /// This is a convenience method for migrating from the previous
352 /// `Option<VarBind>` interface. `None` is treated as `EndOfMibView`.
353 pub fn from_option(value: Option<VarBind>) -> Self {
354 match value {
355 Some(vb) => GetNextResult::Value(vb),
356 None => GetNextResult::EndOfMibView,
357 }
358 }
359
360 /// Returns `true` if this is a value result.
361 pub fn is_value(&self) -> bool {
362 matches!(self, GetNextResult::Value(_))
363 }
364
365 /// Returns `true` if this is end of MIB view.
366 pub fn is_end_of_mib_view(&self) -> bool {
367 matches!(self, GetNextResult::EndOfMibView)
368 }
369
370 /// Converts to an `Option<VarBind>`.
371 pub fn into_option(self) -> Option<VarBind> {
372 match self {
373 GetNextResult::Value(vb) => Some(vb),
374 GetNextResult::EndOfMibView => None,
375 }
376 }
377}
378
379impl From<VarBind> for GetNextResult {
380 fn from(vb: VarBind) -> Self {
381 GetNextResult::Value(vb)
382 }
383}
384
385impl From<Option<VarBind>> for GetNextResult {
386 fn from(value: Option<VarBind>) -> Self {
387 GetNextResult::from_option(value)
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::oid;
395
396 #[test]
397 fn test_response_success() {
398 let response = Response::success(vec![VarBind::new(oid!(1, 3, 6, 1), Value::Integer(1))]);
399 assert_eq!(response.error_status, ErrorStatus::NoError);
400 assert_eq!(response.error_index, 0);
401 assert_eq!(response.varbinds.len(), 1);
402 }
403
404 #[test]
405 fn test_response_error() {
406 let response = Response::error(
407 ErrorStatus::NoSuchName,
408 1,
409 vec![VarBind::new(oid!(1, 3, 6, 1), Value::Null)],
410 );
411 assert_eq!(response.error_status, ErrorStatus::NoSuchName);
412 assert_eq!(response.error_index, 1);
413 }
414
415 #[test]
416 fn test_get_result_from_option() {
417 let result = GetResult::from_option(Some(Value::Integer(42)));
418 assert!(matches!(result, GetResult::Value(Value::Integer(42))));
419
420 let result = GetResult::from_option(None);
421 assert!(matches!(result, GetResult::NoSuchObject));
422 }
423
424 #[test]
425 fn test_get_result_from_value() {
426 let result: GetResult = Value::Integer(42).into();
427 assert!(matches!(result, GetResult::Value(Value::Integer(42))));
428 }
429
430 #[test]
431 fn test_get_next_result_from_option() {
432 let vb = VarBind::new(oid!(1, 3, 6, 1), Value::Integer(42));
433 let result = GetNextResult::from_option(Some(vb.clone()));
434 assert!(result.is_value());
435 assert_eq!(result.into_option(), Some(vb));
436
437 let result = GetNextResult::from_option(None);
438 assert!(result.is_end_of_mib_view());
439 assert_eq!(result.into_option(), None);
440 }
441
442 #[test]
443 fn test_get_next_result_from_varbind() {
444 let vb = VarBind::new(oid!(1, 3, 6, 1), Value::Integer(42));
445 let result: GetNextResult = vb.clone().into();
446 assert!(result.is_value());
447 if let GetNextResult::Value(inner) = result {
448 assert_eq!(inner.oid, oid!(1, 3, 6, 1));
449 }
450 }
451
452 #[test]
453 fn test_set_result_to_error_status() {
454 assert_eq!(SetResult::Ok.to_error_status(), ErrorStatus::NoError);
455 assert_eq!(SetResult::NoAccess.to_error_status(), ErrorStatus::NoAccess);
456 assert_eq!(
457 SetResult::NotWritable.to_error_status(),
458 ErrorStatus::NotWritable
459 );
460 assert_eq!(
461 SetResult::WrongType.to_error_status(),
462 ErrorStatus::WrongType
463 );
464 assert_eq!(
465 SetResult::CommitFailed.to_error_status(),
466 ErrorStatus::CommitFailed
467 );
468 }
469
470 #[test]
471 fn test_set_result_is_ok() {
472 assert!(SetResult::Ok.is_ok());
473 assert!(!SetResult::NoAccess.is_ok());
474 assert!(!SetResult::NotWritable.is_ok());
475 }
476}