Skip to main content

async_snmp/error/
mod.rs

1//! Error types for async-snmp.
2//!
3//! This module provides:
4//!
5//! - [`Error`] - The main error type (8 variants covering all failure modes)
6//! - [`ErrorStatus`] - SNMP protocol errors returned by agents (RFC 3416)
7//! - [`WalkAbortReason`] - Reasons a walk operation was aborted
8//!
9//! # Error Handling
10//!
11//! Errors are boxed for efficiency: `Result<T> = Result<T, Box<Error>>`.
12//!
13//! ```rust
14//! use async_snmp::{Error, Result};
15//!
16//! fn handle_error(result: Result<()>) {
17//!     match result {
18//!         Ok(()) => println!("Success"),
19//!         Err(e) => match &*e {
20//!             Error::Timeout { target, retries, .. } => {
21//!                 println!("{} unreachable after {} retries", target, retries);
22//!             }
23//!             Error::Auth { target } => {
24//!                 println!("Authentication failed for {}", target);
25//!             }
26//!             _ => println!("Error: {}", e),
27//!         }
28//!     }
29//! }
30//! ```
31
32pub(crate) mod internal;
33
34use std::net::SocketAddr;
35use std::time::Duration;
36
37use crate::oid::Oid;
38
39/// Placeholder target address used when no target is known.
40///
41/// This sentinel value (0.0.0.0:0) is used in error contexts where the
42/// target address cannot be determined (e.g., parsing failures before
43/// the source address is known).
44pub(crate) const UNKNOWN_TARGET: SocketAddr =
45    SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0);
46
47// Pattern for converting detailed internal errors to simplified public errors:
48//
49// tracing::debug!(
50//     target: "async_snmp::ber",  // or ::auth, ::crypto, etc.
51//     { snmp.offset = 42, snmp.decode_error = "ZeroLengthInteger" },
52//     "decode error details here"
53// );
54// return Err(Error::MalformedResponse { target }.boxed());
55
56/// Result type alias using the library's boxed Error type.
57pub type Result<T> = std::result::Result<T, Box<Error>>;
58
59/// Reason a walk operation was aborted.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum WalkAbortReason {
62    /// Agent returned an OID that is not greater than the previous OID.
63    NonIncreasing,
64    /// Agent returned an OID that was already seen (cycle detected).
65    Cycle,
66}
67
68impl std::fmt::Display for WalkAbortReason {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            Self::NonIncreasing => write!(f, "non-increasing OID"),
72            Self::Cycle => write!(f, "cycle detected"),
73        }
74    }
75}
76
77impl std::error::Error for WalkAbortReason {}
78
79/// The main error type for all async-snmp operations.
80///
81/// This enum covers all possible error conditions including network issues,
82/// protocol errors, authentication failures, and configuration problems.
83///
84/// Errors are boxed (via [`Result`]) to keep the size small on the stack.
85///
86/// # Common Patterns
87///
88/// ## Checking Error Type
89///
90/// Use pattern matching to handle specific error conditions:
91///
92/// ```
93/// use async_snmp::{Error, ErrorStatus};
94///
95/// fn is_retriable(error: &Error) -> bool {
96///     matches!(error,
97///         Error::Timeout { .. } |
98///         Error::Network { .. }
99///     )
100/// }
101///
102/// fn is_access_error(error: &Error) -> bool {
103///     matches!(error,
104///         Error::Snmp { status: ErrorStatus::NoAccess | ErrorStatus::AuthorizationError, .. } |
105///         Error::Auth { .. }
106///     )
107/// }
108/// ```
109#[derive(Debug, thiserror::Error)]
110#[non_exhaustive]
111pub enum Error {
112    /// Network failure (connection refused, unreachable, etc.)
113    #[error("network error communicating with {target}: {source}")]
114    Network {
115        target: SocketAddr,
116        #[source]
117        source: std::io::Error,
118    },
119
120    /// Request timed out after retries.
121    #[error("timeout after {elapsed:?} waiting for {target} ({retries} retries)")]
122    Timeout {
123        target: SocketAddr,
124        elapsed: Duration,
125        retries: u32,
126    },
127
128    /// Transport was shut down while a request was pending.
129    ///
130    /// Not retriable: the transport will never deliver a response again.
131    /// Recovery requires creating a new transport.
132    #[error("transport closed while waiting for {target}")]
133    Closed { target: SocketAddr },
134
135    /// SNMP protocol error from agent.
136    #[error("SNMP error from {target}: {status} at index {index}")]
137    Snmp {
138        target: SocketAddr,
139        status: ErrorStatus,
140        index: u32,
141        oid: Option<Oid>,
142    },
143
144    /// Authentication/authorization failed.
145    #[error("authentication failed for {target}")]
146    Auth { target: SocketAddr },
147
148    /// Malformed response from agent.
149    #[error("malformed response from {target}")]
150    MalformedResponse { target: SocketAddr },
151
152    /// Walk aborted due to agent misbehavior.
153    #[error("walk aborted for {target}: {reason}")]
154    WalkAborted {
155        target: SocketAddr,
156        reason: WalkAbortReason,
157    },
158
159    /// Invalid configuration.
160    #[error("configuration error: {0}")]
161    Config(Box<str>),
162
163    /// Invalid OID format.
164    #[error("invalid OID: {0}")]
165    InvalidOid(Box<str>),
166}
167
168impl Error {
169    /// Box this error (convenience for constructing boxed errors).
170    #[must_use]
171    pub fn boxed(self) -> Box<Self> {
172        Box::new(self)
173    }
174}
175
176/// SNMP protocol error status codes (RFC 3416).
177///
178/// These codes are returned by SNMP agents to indicate the result of an operation.
179/// The error status is included in the [`Error::Snmp`] variant along with an error
180/// index indicating which varbind caused the error.
181///
182/// # Error Categories
183///
184/// ## `SNMPv1` Errors (0-5)
185///
186/// - `NoError` - Operation succeeded
187/// - `TooBig` - Response too large for transport
188/// - `NoSuchName` - OID not found (v1 only; v2c+ uses exceptions)
189/// - `BadValue` - Invalid value in SET
190/// - `ReadOnly` - Attempted write to read-only object
191/// - `GenErr` - Unspecified error
192///
193/// ## SNMPv2c/v3 Errors (6-18)
194///
195/// These provide more specific error information for SET operations:
196///
197/// - `NoAccess` - Object not accessible (access control)
198/// - `WrongType` - Value has wrong ASN.1 type
199/// - `WrongLength` - Value has wrong length
200/// - `WrongValue` - Value out of range or invalid
201/// - `NotWritable` - Object does not support SET
202/// - `AuthorizationError` - Access denied by VACM
203///
204/// # Example
205///
206/// ```
207/// use async_snmp::ErrorStatus;
208///
209/// let status = ErrorStatus::from_i32(2);
210/// assert_eq!(status, ErrorStatus::NoSuchName);
211/// assert_eq!(status.as_i32(), 2);
212/// println!("Error: {}", status); // prints "noSuchName"
213/// ```
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
215#[non_exhaustive]
216pub enum ErrorStatus {
217    /// Operation completed successfully (status = 0).
218    NoError,
219    /// Response message would be too large for transport (status = 1).
220    TooBig,
221    /// Requested OID not found (status = 2). `SNMPv1` only; v2c+ uses exception values.
222    NoSuchName,
223    /// Invalid value provided in SET request (status = 3).
224    BadValue,
225    /// Attempted to SET a read-only object (status = 4).
226    ReadOnly,
227    /// Unspecified error occurred (status = 5).
228    GenErr,
229    /// Object exists but access is denied (status = 6).
230    NoAccess,
231    /// SET value has wrong ASN.1 type (status = 7).
232    WrongType,
233    /// SET value has incorrect length (status = 8).
234    WrongLength,
235    /// SET value uses wrong encoding (status = 9).
236    WrongEncoding,
237    /// SET value is out of range or otherwise invalid (status = 10).
238    WrongValue,
239    /// Object does not support row creation (status = 11).
240    NoCreation,
241    /// Value is inconsistent with other managed objects (status = 12).
242    InconsistentValue,
243    /// Resource required for SET is unavailable (status = 13).
244    ResourceUnavailable,
245    /// SET commit phase failed (status = 14).
246    CommitFailed,
247    /// SET undo phase failed (status = 15).
248    UndoFailed,
249    /// Access denied by VACM (status = 16).
250    AuthorizationError,
251    /// Object does not support modification (status = 17).
252    NotWritable,
253    /// Named object cannot be created (status = 18).
254    InconsistentName,
255    /// Unknown or future error status code.
256    Unknown(i32),
257}
258
259impl ErrorStatus {
260    /// Create from raw status code.
261    pub fn from_i32(value: i32) -> Self {
262        match value {
263            0 => Self::NoError,
264            1 => Self::TooBig,
265            2 => Self::NoSuchName,
266            3 => Self::BadValue,
267            4 => Self::ReadOnly,
268            5 => Self::GenErr,
269            6 => Self::NoAccess,
270            7 => Self::WrongType,
271            8 => Self::WrongLength,
272            9 => Self::WrongEncoding,
273            10 => Self::WrongValue,
274            11 => Self::NoCreation,
275            12 => Self::InconsistentValue,
276            13 => Self::ResourceUnavailable,
277            14 => Self::CommitFailed,
278            15 => Self::UndoFailed,
279            16 => Self::AuthorizationError,
280            17 => Self::NotWritable,
281            18 => Self::InconsistentName,
282            other => {
283                tracing::warn!(target: "async_snmp::error", { snmp.error_status = other }, "unknown SNMP error status");
284                Self::Unknown(other)
285            }
286        }
287    }
288
289    /// Convert to raw status code.
290    #[must_use]
291    pub fn as_i32(&self) -> i32 {
292        match self {
293            Self::NoError => 0,
294            Self::TooBig => 1,
295            Self::NoSuchName => 2,
296            Self::BadValue => 3,
297            Self::ReadOnly => 4,
298            Self::GenErr => 5,
299            Self::NoAccess => 6,
300            Self::WrongType => 7,
301            Self::WrongLength => 8,
302            Self::WrongEncoding => 9,
303            Self::WrongValue => 10,
304            Self::NoCreation => 11,
305            Self::InconsistentValue => 12,
306            Self::ResourceUnavailable => 13,
307            Self::CommitFailed => 14,
308            Self::UndoFailed => 15,
309            Self::AuthorizationError => 16,
310            Self::NotWritable => 17,
311            Self::InconsistentName => 18,
312            Self::Unknown(code) => *code,
313        }
314    }
315
316    /// Map a v2c+ error status to its v1 equivalent per RFC 2576 Section 4.3.
317    ///
318    /// V1-native statuses (0-5) pass through unchanged.
319    #[must_use]
320    pub fn to_v1(&self) -> Self {
321        match self {
322            // V1-native statuses
323            Self::NoError
324            | Self::TooBig
325            | Self::NoSuchName
326            | Self::BadValue
327            | Self::ReadOnly
328            | Self::GenErr => *self,
329
330            // Value errors -> BadValue
331            Self::WrongType
332            | Self::WrongLength
333            | Self::WrongEncoding
334            | Self::WrongValue
335            | Self::InconsistentValue => Self::BadValue,
336
337            // Access/creation errors -> NoSuchName
338            Self::NoAccess
339            | Self::NotWritable
340            | Self::NoCreation
341            | Self::InconsistentName
342            | Self::AuthorizationError => Self::NoSuchName,
343
344            // Resource/commit errors -> GenErr
345            Self::ResourceUnavailable | Self::CommitFailed | Self::UndoFailed => Self::GenErr,
346
347            Self::Unknown(_) => Self::GenErr,
348        }
349    }
350
351    /// Return the canonical SMI name for this status code.
352    ///
353    /// For `Unknown` variants, returns `None`; callers should format the
354    /// numeric code directly in that case.
355    #[must_use]
356    pub fn as_str(&self) -> Option<&'static str> {
357        match self {
358            Self::NoError => Some("noError"),
359            Self::TooBig => Some("tooBig"),
360            Self::NoSuchName => Some("noSuchName"),
361            Self::BadValue => Some("badValue"),
362            Self::ReadOnly => Some("readOnly"),
363            Self::GenErr => Some("genErr"),
364            Self::NoAccess => Some("noAccess"),
365            Self::WrongType => Some("wrongType"),
366            Self::WrongLength => Some("wrongLength"),
367            Self::WrongEncoding => Some("wrongEncoding"),
368            Self::WrongValue => Some("wrongValue"),
369            Self::NoCreation => Some("noCreation"),
370            Self::InconsistentValue => Some("inconsistentValue"),
371            Self::ResourceUnavailable => Some("resourceUnavailable"),
372            Self::CommitFailed => Some("commitFailed"),
373            Self::UndoFailed => Some("undoFailed"),
374            Self::AuthorizationError => Some("authorizationError"),
375            Self::NotWritable => Some("notWritable"),
376            Self::InconsistentName => Some("inconsistentName"),
377            Self::Unknown(_) => None,
378        }
379    }
380}
381
382impl std::fmt::Display for ErrorStatus {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        match self.as_str() {
385            Some(name) => f.write_str(name),
386            None => write!(f, "unknown({})", self.as_i32()),
387        }
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn walk_abort_reason_is_error() {
397        let reason = WalkAbortReason::NonIncreasing;
398        let err: &dyn std::error::Error = &reason;
399        assert_eq!(err.to_string(), "non-increasing OID");
400    }
401
402    #[test]
403    fn error_status_to_v1_mapping() {
404        // RFC 2576 Section 4.3 mappings
405        // V1 statuses (0-5) pass through unchanged
406        assert_eq!(ErrorStatus::NoError.to_v1(), ErrorStatus::NoError);
407        assert_eq!(ErrorStatus::TooBig.to_v1(), ErrorStatus::TooBig);
408        assert_eq!(ErrorStatus::NoSuchName.to_v1(), ErrorStatus::NoSuchName);
409        assert_eq!(ErrorStatus::BadValue.to_v1(), ErrorStatus::BadValue);
410        assert_eq!(ErrorStatus::ReadOnly.to_v1(), ErrorStatus::ReadOnly);
411        assert_eq!(ErrorStatus::GenErr.to_v1(), ErrorStatus::GenErr);
412
413        // WrongValue/WrongType/WrongLength/WrongEncoding/InconsistentValue -> BadValue
414        assert_eq!(ErrorStatus::WrongValue.to_v1(), ErrorStatus::BadValue);
415        assert_eq!(ErrorStatus::WrongType.to_v1(), ErrorStatus::BadValue);
416        assert_eq!(ErrorStatus::WrongLength.to_v1(), ErrorStatus::BadValue);
417        assert_eq!(ErrorStatus::WrongEncoding.to_v1(), ErrorStatus::BadValue);
418        assert_eq!(
419            ErrorStatus::InconsistentValue.to_v1(),
420            ErrorStatus::BadValue
421        );
422
423        // NoAccess/NotWritable/NoCreation/InconsistentName/AuthorizationError -> NoSuchName
424        assert_eq!(ErrorStatus::NoAccess.to_v1(), ErrorStatus::NoSuchName);
425        assert_eq!(ErrorStatus::NotWritable.to_v1(), ErrorStatus::NoSuchName);
426        assert_eq!(ErrorStatus::NoCreation.to_v1(), ErrorStatus::NoSuchName);
427        assert_eq!(
428            ErrorStatus::InconsistentName.to_v1(),
429            ErrorStatus::NoSuchName
430        );
431        assert_eq!(
432            ErrorStatus::AuthorizationError.to_v1(),
433            ErrorStatus::NoSuchName
434        );
435
436        // ResourceUnavailable/CommitFailed/UndoFailed -> GenErr
437        assert_eq!(
438            ErrorStatus::ResourceUnavailable.to_v1(),
439            ErrorStatus::GenErr
440        );
441        assert_eq!(ErrorStatus::CommitFailed.to_v1(), ErrorStatus::GenErr);
442        assert_eq!(ErrorStatus::UndoFailed.to_v1(), ErrorStatus::GenErr);
443    }
444
445    #[test]
446    fn error_size_budget() {
447        // Error size should stay bounded to avoid bloating Result types.
448        // The largest variant is Error::Snmp which contains Option<Oid>.
449        assert!(
450            std::mem::size_of::<Error>() <= 128,
451            "Error size {} exceeds 128-byte budget",
452            std::mem::size_of::<Error>()
453        );
454
455        // Result<(), Box<Error>> should be pointer-sized (8 bytes on 64-bit).
456        assert_eq!(
457            std::mem::size_of::<Result<()>>(),
458            std::mem::size_of::<*const ()>(),
459            "Result<()> should be pointer-sized"
460        );
461    }
462}