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::new(0, 0, 0, 0)), 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)]
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
77/// The main error type for all async-snmp operations.
78///
79/// This enum covers all possible error conditions including network issues,
80/// protocol errors, authentication failures, and configuration problems.
81///
82/// Errors are boxed (via [`Result`]) to keep the size small on the stack.
83///
84/// # Common Patterns
85///
86/// ## Checking Error Type
87///
88/// Use pattern matching to handle specific error conditions:
89///
90/// ```
91/// use async_snmp::{Error, ErrorStatus};
92///
93/// fn is_retriable(error: &Error) -> bool {
94///     matches!(error,
95///         Error::Timeout { .. } |
96///         Error::Network { .. }
97///     )
98/// }
99///
100/// fn is_access_error(error: &Error) -> bool {
101///     matches!(error,
102///         Error::Snmp { status: ErrorStatus::NoAccess | ErrorStatus::AuthorizationError, .. } |
103///         Error::Auth { .. }
104///     )
105/// }
106/// ```
107#[derive(Debug, thiserror::Error)]
108#[non_exhaustive]
109pub enum Error {
110    /// Network failure (connection refused, unreachable, etc.)
111    #[error("network error communicating with {target}: {source}")]
112    Network {
113        target: SocketAddr,
114        #[source]
115        source: std::io::Error,
116    },
117
118    /// Request timed out after retries.
119    #[error("timeout after {elapsed:?} waiting for {target} ({retries} retries)")]
120    Timeout {
121        target: SocketAddr,
122        elapsed: Duration,
123        retries: u32,
124    },
125
126    /// SNMP protocol error from agent.
127    #[error("SNMP error from {target}: {status} at index {index}")]
128    Snmp {
129        target: SocketAddr,
130        status: ErrorStatus,
131        index: u32,
132        oid: Option<Oid>,
133    },
134
135    /// Authentication/authorization failed.
136    #[error("authentication failed for {target}")]
137    Auth { target: SocketAddr },
138
139    /// Malformed response from agent.
140    #[error("malformed response from {target}")]
141    MalformedResponse { target: SocketAddr },
142
143    /// Walk aborted due to agent misbehavior.
144    #[error("walk aborted for {target}: {reason}")]
145    WalkAborted {
146        target: SocketAddr,
147        reason: WalkAbortReason,
148    },
149
150    /// Invalid configuration.
151    #[error("configuration error: {0}")]
152    Config(Box<str>),
153
154    /// Invalid OID format.
155    #[error("invalid OID: {0}")]
156    InvalidOid(Box<str>),
157}
158
159impl Error {
160    /// Box this error (convenience for constructing boxed errors).
161    pub fn boxed(self) -> Box<Self> {
162        Box::new(self)
163    }
164}
165
166/// SNMP protocol error status codes (RFC 3416).
167///
168/// These codes are returned by SNMP agents to indicate the result of an operation.
169/// The error status is included in the [`Error::Snmp`] variant along with an error
170/// index indicating which varbind caused the error.
171///
172/// # Error Categories
173///
174/// ## SNMPv1 Errors (0-5)
175///
176/// - `NoError` - Operation succeeded
177/// - `TooBig` - Response too large for transport
178/// - `NoSuchName` - OID not found (v1 only; v2c+ uses exceptions)
179/// - `BadValue` - Invalid value in SET
180/// - `ReadOnly` - Attempted write to read-only object
181/// - `GenErr` - Unspecified error
182///
183/// ## SNMPv2c/v3 Errors (6-18)
184///
185/// These provide more specific error information for SET operations:
186///
187/// - `NoAccess` - Object not accessible (access control)
188/// - `WrongType` - Value has wrong ASN.1 type
189/// - `WrongLength` - Value has wrong length
190/// - `WrongValue` - Value out of range or invalid
191/// - `NotWritable` - Object does not support SET
192/// - `AuthorizationError` - Access denied by VACM
193///
194/// # Example
195///
196/// ```
197/// use async_snmp::ErrorStatus;
198///
199/// let status = ErrorStatus::from_i32(2);
200/// assert_eq!(status, ErrorStatus::NoSuchName);
201/// assert_eq!(status.as_i32(), 2);
202/// println!("Error: {}", status); // prints "noSuchName"
203/// ```
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205#[non_exhaustive]
206pub enum ErrorStatus {
207    /// Operation completed successfully (status = 0).
208    NoError,
209    /// Response message would be too large for transport (status = 1).
210    TooBig,
211    /// Requested OID not found (status = 2). SNMPv1 only; v2c+ uses exception values.
212    NoSuchName,
213    /// Invalid value provided in SET request (status = 3).
214    BadValue,
215    /// Attempted to SET a read-only object (status = 4).
216    ReadOnly,
217    /// Unspecified error occurred (status = 5).
218    GenErr,
219    /// Object exists but access is denied (status = 6).
220    NoAccess,
221    /// SET value has wrong ASN.1 type (status = 7).
222    WrongType,
223    /// SET value has incorrect length (status = 8).
224    WrongLength,
225    /// SET value uses wrong encoding (status = 9).
226    WrongEncoding,
227    /// SET value is out of range or otherwise invalid (status = 10).
228    WrongValue,
229    /// Object does not support row creation (status = 11).
230    NoCreation,
231    /// Value is inconsistent with other managed objects (status = 12).
232    InconsistentValue,
233    /// Resource required for SET is unavailable (status = 13).
234    ResourceUnavailable,
235    /// SET commit phase failed (status = 14).
236    CommitFailed,
237    /// SET undo phase failed (status = 15).
238    UndoFailed,
239    /// Access denied by VACM (status = 16).
240    AuthorizationError,
241    /// Object does not support modification (status = 17).
242    NotWritable,
243    /// Named object cannot be created (status = 18).
244    InconsistentName,
245    /// Unknown or future error status code.
246    Unknown(i32),
247}
248
249impl ErrorStatus {
250    /// Create from raw status code.
251    pub fn from_i32(value: i32) -> Self {
252        match value {
253            0 => Self::NoError,
254            1 => Self::TooBig,
255            2 => Self::NoSuchName,
256            3 => Self::BadValue,
257            4 => Self::ReadOnly,
258            5 => Self::GenErr,
259            6 => Self::NoAccess,
260            7 => Self::WrongType,
261            8 => Self::WrongLength,
262            9 => Self::WrongEncoding,
263            10 => Self::WrongValue,
264            11 => Self::NoCreation,
265            12 => Self::InconsistentValue,
266            13 => Self::ResourceUnavailable,
267            14 => Self::CommitFailed,
268            15 => Self::UndoFailed,
269            16 => Self::AuthorizationError,
270            17 => Self::NotWritable,
271            18 => Self::InconsistentName,
272            other => {
273                tracing::warn!(target: "async_snmp::error", { snmp.error_status = other }, "unknown SNMP error status");
274                Self::Unknown(other)
275            }
276        }
277    }
278
279    /// Convert to raw status code.
280    pub fn as_i32(&self) -> i32 {
281        match self {
282            Self::NoError => 0,
283            Self::TooBig => 1,
284            Self::NoSuchName => 2,
285            Self::BadValue => 3,
286            Self::ReadOnly => 4,
287            Self::GenErr => 5,
288            Self::NoAccess => 6,
289            Self::WrongType => 7,
290            Self::WrongLength => 8,
291            Self::WrongEncoding => 9,
292            Self::WrongValue => 10,
293            Self::NoCreation => 11,
294            Self::InconsistentValue => 12,
295            Self::ResourceUnavailable => 13,
296            Self::CommitFailed => 14,
297            Self::UndoFailed => 15,
298            Self::AuthorizationError => 16,
299            Self::NotWritable => 17,
300            Self::InconsistentName => 18,
301            Self::Unknown(code) => *code,
302        }
303    }
304}
305
306impl std::fmt::Display for ErrorStatus {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        match self {
309            Self::NoError => write!(f, "noError"),
310            Self::TooBig => write!(f, "tooBig"),
311            Self::NoSuchName => write!(f, "noSuchName"),
312            Self::BadValue => write!(f, "badValue"),
313            Self::ReadOnly => write!(f, "readOnly"),
314            Self::GenErr => write!(f, "genErr"),
315            Self::NoAccess => write!(f, "noAccess"),
316            Self::WrongType => write!(f, "wrongType"),
317            Self::WrongLength => write!(f, "wrongLength"),
318            Self::WrongEncoding => write!(f, "wrongEncoding"),
319            Self::WrongValue => write!(f, "wrongValue"),
320            Self::NoCreation => write!(f, "noCreation"),
321            Self::InconsistentValue => write!(f, "inconsistentValue"),
322            Self::ResourceUnavailable => write!(f, "resourceUnavailable"),
323            Self::CommitFailed => write!(f, "commitFailed"),
324            Self::UndoFailed => write!(f, "undoFailed"),
325            Self::AuthorizationError => write!(f, "authorizationError"),
326            Self::NotWritable => write!(f, "notWritable"),
327            Self::InconsistentName => write!(f, "inconsistentName"),
328            Self::Unknown(code) => write!(f, "unknown({})", code),
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn error_size_budget() {
339        // Error size should stay bounded to avoid bloating Result types.
340        // The largest variant is Error::Snmp which contains Option<Oid>.
341        assert!(
342            std::mem::size_of::<Error>() <= 128,
343            "Error size {} exceeds 128-byte budget",
344            std::mem::size_of::<Error>()
345        );
346
347        // Result<(), Box<Error>> should be pointer-sized (8 bytes on 64-bit).
348        assert_eq!(
349            std::mem::size_of::<Result<()>>(),
350            std::mem::size_of::<*const ()>(),
351            "Result<()> should be pointer-sized"
352        );
353    }
354}