# iReasoning SNMP SDK for Rust

Developer Guide and API Reference — SDK 1.0, ABI v1

This guide explains how a Rust application uses the binary iReasoning SNMP SDK. It covers the complete supported safe Rust API: SNMP GET, GET-NEXT, SET, GET-BULK, walk and subtree traversal, SNMP notification reception, and SMIv1/SMIv2 MIB parsing and lookup.

The SDK is distributed as compiled native libraries plus three small source crates:

- `ireasoning-snmp-sdk` is the safe Rust API applications should use.
- `ireasoning-snmp-sdk-types` contains the public data models re-exported by the safe crate.
- `ireasoning-snmp-sdk-sys` connects the safe crate to the native static or dynamic library.

Applications do not compile or receive the proprietary SNMP and MIB implementation sources. Rust layouts never cross the binary boundary. The native library uses ABI-major-versioned C symbols and versioned JSON payloads internally, while `ireasoning-snmp-sdk` presents ordinary typed Rust functions.

## 1. Requirements and supported targets

- Rust 1.85 or newer.
- A target-specific SDK archive matching the final application target.
- No JDK, JNI runtime, native cryptography package, or external ASN.1 library.
- Tokio is required by the safe crate for the async façade. Blocking-only applications do not need to create a Tokio runtime.

Supported SDK targets are:

| Target | Static library | Dynamic library |
|---|---|---|
| `x86_64-unknown-linux-gnu` | `.a` | `.so` |
| `aarch64-unknown-linux-gnu` | `.a` | `.so` |
| `x86_64-apple-darwin` | `.a` | `.dylib` |
| `aarch64-apple-darwin` | `.a` | `.dylib` |
| `x86_64-pc-windows-msvc` | `.lib` | `.dll` |

## 2. Installing the SDK

Unpack the SDK archive under a stable directory in the application repository, for example:

```text
my-application/
├── Cargo.toml
├── src/
└── vendor/
    └── ireasoning-snmp-sdk/
        ├── lib/
        ├── include/
        ├── docs/
        └── rust/
            └── crates/
                ├── sdk/
                ├── sdk-sys/
                └── sdk-types/
```

Add the safe crate to the application:

```toml
[dependencies]
ireasoning-snmp-sdk = {
    path = "vendor/ireasoning-snmp-sdk/rust/crates/sdk"
}
```

Static linking is the distribution default. To link the dynamic library instead:

```toml
[dependencies]
ireasoning-snmp-sdk = {
    path = "vendor/ireasoning-snmp-sdk/rust/crates/sdk",
    default-features = false,
    features = ["dynamic"]
}
```

The packaged `sdk-sys` crate normally finds the native libraries in its adjacent `native` directory. If the libraries are installed elsewhere, set `IREASONING_SNMP_SDK_DIR` while building:

```text
IREASONING_SNMP_SDK_DIR=/opt/ireasoning-snmp-sdk/lib cargo build --release
```

For dynamic linking, also make the shared library available to the operating-system loader at deployment time. Typical mechanisms are `LD_LIBRARY_PATH` or an rpath on Linux, `DYLD_LIBRARY_PATH` or an application-bundle location on macOS, and `PATH` or the executable directory on Windows.

Do not enable `static` and `dynamic` together. Use one target archive and one linkage mode for a final executable.

Applications that use the async examples should also declare Tokio directly so their own crate can use the runtime macro:

```toml
[dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

Generated Rust API documentation is included at `docs/ireasoning_snmp_sdk/index.html` in each archive. This guide explains behavior and integration; the generated pages are useful for type-by-type navigation in an editor or browser.

## 3. First program

The following program performs an SNMPv2c GET:

```rust,no_run
use ireasoning_snmp_sdk::{
    BlockingClient, ClientConfig, Credentials, Oid, Result,
};

fn main() -> Result<()> {
    let config = ClientConfig::new(
        "router.example.com",
        Credentials::v2c("public"),
    );
    let mut client = BlockingClient::connect(config)?;

    let oid = ".1.3.6.1.2.1.1.3.0".parse::<Oid>().unwrap();
    let response = client.get(&[oid])?;

    for varbind in response.pdu.varbinds {
        println!("{} = {:?}", varbind.oid, varbind.value);
    }
    Ok(())
}
```

The `?` operator works because all operational functions return the SDK alias `Result<T>`, which is `std::result::Result<T, SdkError>`.

## 4. ABI and build information

### `build_info`

```rust
pub fn build_info() -> Result<BuildInfo>
```

Validates the linked native ABI and JSON schema, then returns the native build identity. The safe bindings perform this check automatically before creating or using a native handle, so calling it explicitly is optional but useful for startup diagnostics.

`BuildInfo` fields are:

| Field | Meaning |
|---|---|
| `sdk_version: String` | Native SDK semantic version. |
| `abi_major: u32` | Native ABI major version. Version 1.0 uses `1`. |
| `json_schema_version: u32` | Complex-payload schema version. Version 1.0 uses `1`. |
| `target: String` | Architecture and operating-system identity compiled into the library. |
| `profile: String` | Native build profile, normally `release`. |

`JSON_SCHEMA_VERSION` is a public constant containing the schema version expected by the Rust bindings.

Example:

```rust,no_run
use ireasoning_snmp_sdk::{build_info, Result};

fn main() -> Result<()> {
    let info = build_info()?;
    println!(
        "SDK {} / ABI {} / schema {} / {}",
        info.sdk_version,
        info.abi_major,
        info.json_schema_version,
        info.target,
    );
    Ok(())
}
```

An incompatible library produces `ErrorKind::IncompatibleAbi` before an operational handle is created.

## 5. Error handling

### `Result<T>`

```rust
pub type Result<T> = std::result::Result<T, SdkError>;
```

### `SdkError`

| Field | Meaning |
|---|---|
| `kind: ErrorKind` | Stable category suitable for program control flow. |
| `message: String` | Human-readable, credential-redacted description. |
| `status: Option<i32>` | SNMP Response-PDU error status when `kind` is `SnmpResponse`. |
| `index: Option<i32>` | One-based offending varbind index supplied by the agent, when available. |

`SdkError` implements `std::error::Error` and `Display`. Its `Debug` and `Display` output do not include communities, passwords, localized keys, or privacy salts.

`ErrorKind` values are:

| Kind | Typical cause |
|---|---|
| `Argument` | Invalid OID, empty input, zero receive timeout, or malformed binding data. |
| `Configuration` | Unsupported version/operation combination, invalid security configuration, limit violation, or oversized outbound message. |
| `Timeout` | No matching response or notification before the timeout. |
| `Transport` | DNS, socket, TCP framing, worker, or other I/O failure. |
| `SnmpResponse` | Agent Response-PDU error, malformed SNMP input, or protocol mismatch. |
| `Usm` | SNMPv3 report, authentication, privacy, timeliness, or notification security failure. |
| `Mib` | MIB I/O, parsing, import, ambiguity, or resolution failure. |
| `IncompatibleAbi` | Native ABI or JSON schema is incompatible with the bindings. |
| `InvalidHandle` | A stale or wrong-type native handle was detected. This normally indicates an SDK integration defect. |
| `Callback` | A traversal callback failed or panicked. |
| `InternalPanic` | A panic or invariant failure was contained inside the SDK boundary. |

Response errors can be handled as follows:

```rust,no_run
use ireasoning_snmp_sdk::{
    BlockingClient, ClientConfig, Credentials, ErrorKind, ErrorStatus,
    Oid, Result,
};

fn read_value(client: &mut BlockingClient, oid: Oid) -> Result<()> {
    match client.get(&[oid]) {
        Ok(response) => println!("{:?}", response.pdu.varbinds),
        Err(error) if error.kind == ErrorKind::SnmpResponse => {
            if let Some(code) = error.status {
                eprintln!(
                    "agent error {:?} at varbind {:?}",
                    ErrorStatus::from_code(code),
                    error.index,
                );
            } else {
                eprintln!("protocol error: {error}");
            }
        }
        Err(error) => return Err(error),
    }
    Ok(())
}
# let _ = (ClientConfig::new("localhost", Credentials::v2c("public")), read_value);
```

### Error helper functions

```rust
SdkError::new(kind: ErrorKind, message: impl Into<String>) -> SdkError
```

Creates an SDK-style error without an SNMP response status or index. This is useful when an application callback needs to translate its own failure into the SDK error model.

```rust
SdkError::response(
    message: impl Into<String>,
    status: i32,
    index: i32,
) -> SdkError
```

Creates an `ErrorKind::SnmpResponse` error with a numeric Response-PDU status and one-based error index.

### `ErrorStatus`

`ErrorStatus` represents all standard SNMPv1/SNMPv2 response statuses: `NoError`, `TooBig`, `NoSuchName`, `BadValue`, `ReadOnly`, `GenErr`, `NoAccess`, `WrongType`, `WrongLength`, `WrongEncoding`, `WrongValue`, `NoCreation`, `InconsistentValue`, `ResourceUnavailable`, `CommitFailed`, `UndoFailed`, `AuthorizationError`, `NotWritable`, and `InconsistentName`. Unrecognized numbers are retained as `Unknown(i32)`.

```rust
ErrorStatus::from_code(code: i32) -> ErrorStatus
```

Maps a wire error-status number to the symbolic enum.

```rust
status.code() -> i32
```

Returns the wire number, preserving the value inside `Unknown`.

## 6. OIDs, values, varbinds, PDUs, and responses

### `Oid`

`Oid` is an owned vector of unsigned arcs. It implements `FromStr`, `Display`, ordering, equality, hashing, cloning, and Serde serialization.

```rust
Oid::new(arcs: impl Into<Vec<u32>>) -> std::result::Result<Oid, OidParseError>
```

Constructs an OID from arcs. The first arc must be `0`, `1`, or `2`; when the first arc is `0` or `1`, the second must be at most `39`.

```rust
oid.arcs() -> &[u32]
```

Returns a borrowed view of the numeric arcs.

```rust
oid.is_empty() -> bool
```

Returns whether the OID contains no arcs.

```rust
oid.starts_with(other: &Oid) -> bool
```

Tests whether `other` is an OID prefix of `oid`. This is useful for subtree checks.

```rust
oid.appended(arcs: impl IntoIterator<Item = u32>) -> Oid
```

Returns a cloned OID with additional arcs. The original is unchanged.

Numeric strings with or without the leading dot can be parsed:

```rust
use ireasoning_snmp_sdk::Oid;

let first: Oid = ".1.3.6.1.2.1.1.3.0".parse().unwrap();
let second: Oid = "1.3.6.1.2.1.1.5.0".parse().unwrap();
assert_eq!(first.to_string(), ".1.3.6.1.2.1.1.3.0");
assert!(second.starts_with(&".1.3.6.1.2.1".parse().unwrap()));
```

Symbolic strings such as `sysUpTime.0` are resolved by `MibRegistry::resolve_oid`, not by `Oid::from_str`.

### `Value`

`Value` preserves the SNMP syntax received from or sent to an agent:

| Variant | Rust payload | Use |
|---|---|---|
| `Integer` | `i32` | ASN.1 INTEGER and enumerations. |
| `OctetString` | `Vec<u8>` | OCTET STRING, display strings, and BITS. |
| `Null` | none | Retrieval request placeholder; not legal for SET. |
| `ObjectIdentifier` | `Oid` | OBJECT IDENTIFIER. |
| `IpAddress` | `[u8; 4]` | SNMP IPv4 application value. |
| `Counter32` | `u32` | Counter32. |
| `Gauge32` | `u32` | Gauge32 or Unsigned32 wire value. |
| `TimeTicks` | `u32` | Hundredths of a second. |
| `Opaque` | `Vec<u8>` | Opaque application bytes. |
| `Counter64` | `u64` | Counter64; not legal in an SNMPv1 SET request. |
| `NoSuchObject` | none | SNMPv2 exception response. |
| `NoSuchInstance` | none | SNMPv2 exception response. |
| `EndOfMibView` | none | SNMPv2 traversal termination response. |
| `Unknown` | `{ tag: u8, bytes: Vec<u8> }` | Preserved unknown tag and raw content. Not legal for SET. |

Only `Integer`, `OctetString`, `ObjectIdentifier`, `IpAddress`, `Counter32`, `Gauge32`, `TimeTicks`, `Opaque`, and `Counter64` are accepted by `set`. `Null`, exception values, and `Unknown` are rejected locally before transmission.

### `VarBind`

```rust
pub struct VarBind {
    pub oid: Oid,
    pub value: Value,
}
```

```rust
VarBind::new(oid: Oid, value: Value) -> VarBind
```

Constructs one variable binding. SET preserves the order of the supplied slice, including duplicate OIDs.

### `Pdu`, `PduType`, and `Response`

```rust
pub struct Response {
    pub version: SnmpVersion,
    pub pdu: Pdu,
}
```

`Pdu` fields are:

| Field | Meaning |
|---|---|
| `pdu_type: PduType` | `Get`, `GetNext`, `Set`, `GetBulk`, `Response`, `Inform`, `V2Trap`, or `Report`. Successful manager operations normally return `Response`. |
| `request_id: i32` | Correlation identifier generated by the native client. |
| `error_status: i32` | Numeric agent error status. Nonzero response statuses are normally returned as `SdkError`, rather than as a successful `Response`. |
| `error_index: i32` | Agent-supplied one-based offending varbind index. |
| `varbinds: Vec<VarBind>` | Returned variable bindings in wire order. |

```rust
pdu.response_status() -> ErrorStatus
```

Maps `pdu.error_status` to `ErrorStatus`.

## 7. Client configuration and credentials

### `ClientConfig`

```rust
ClientConfig::new(
    host: impl Into<String>,
    credentials: Credentials,
) -> ClientConfig
```

Creates a configuration with port `161`, UDP, a 5,000 ms timeout, zero retries, and a 32 KiB maximum message size.

All configuration fields are public:

| Field | Default | Meaning |
|---|---:|---|
| `host: String` | supplied | DNS name or numeric address. It must not be empty. For IPv6, pass the address without URL brackets. |
| `port: u16` | `161` | Remote SNMP port. |
| `credentials: Credentials` | supplied | SNMP version and community or USM credentials. |
| `transport: Transport` | `Udp` | `Transport::Udp` or `Transport::Tcp`. TCP uses BER-length message framing. |
| `timeout_ms: u64` | `5000` | Timeout for each transport attempt in milliseconds. It must be greater than zero. |
| `retries: u32` | `0` | Additional attempts after the first timeout. Retries reuse the same encoded request and request ID. |
| `max_message_size: usize` | `32768` | Maximum inbound and outbound SNMP message size. It must be at least 484 bytes. |

Example customization:

```rust
use ireasoning_snmp_sdk::{
    ClientConfig, Credentials, Transport,
};

let mut config = ClientConfig::new(
    "192.0.2.10",
    Credentials::v2c("monitoring"),
);
config.port = 1161;
config.transport = Transport::Tcp;
config.timeout_ms = 3_000;
config.retries = 2;
config.max_message_size = 128 * 1024;
```

### SNMPv1 and SNMPv2c

```rust
Credentials::v1(community: impl Into<String>) -> Credentials
Credentials::v2c(community: impl Into<String>) -> Credentials
```

These constructors select the version and store the supplied community. Communities longer than 255 bytes are rejected. Credential `Debug` output is redacted, and owned community storage is zeroized when dropped.

GET-BULK and bulk traversal are not supported by SNMPv1. The library returns `ErrorKind::Configuration`; unlike the command-line tools, it does not silently upgrade a reusable library request to SNMPv2c.

The related enums are:

- `SnmpVersion::{V1, V2c, V3}` identifies the wire protocol version.
- `Transport::{Udp, Tcp}` selects the manager transport.
- `Credentials::Community { version, community }` is the serialized v1/v2c form created by `v1` and `v2c`.
- `Credentials::V3(UsmCredentials)` is the serialized SNMPv3 form created by `v3`.

Prefer the credential constructors so the version and credential form cannot be accidentally mismatched.

### SNMPv3 USM

```rust
UsmCredentials::no_auth(username: impl Into<String>) -> UsmCredentials
```

Creates `noAuthNoPriv` credentials.

```rust
UsmCredentials::authenticated(
    username: impl Into<String>,
    protocol: AuthProtocol,
    password: impl Into<String>,
) -> UsmCredentials
```

Creates `authNoPriv` credentials. `protocol` may be `Md5`, `Sha1`, `Sha224`, `Sha256`, `Sha384`, or `Sha512`.

```rust
credentials.with_privacy(
    protocol: PrivacyProtocol,
    password: impl Into<String>,
) -> UsmCredentials
```

Adds privacy and produces `authPriv` credentials. `protocol` may be `Des`, `TripleDes`, `Aes128`, `Aes192`, or `Aes256`. Privacy requires authentication.

```rust
credentials.with_context(
    context_name: impl Into<String>,
) -> UsmCredentials
```

Selects an SNMP context name. The default is the empty context.

```rust
credentials.with_context_engine_id(
    engine_id: Vec<u8>,
) -> UsmCredentials
```

Overrides the scoped-PDU context engine ID. When absent, the discovered authoritative engine ID is normally used.

```rust
Credentials::v3(credentials: UsmCredentials) -> Credentials
```

Wraps USM credentials for `ClientConfig`.

The `UsmCredentials` fields are public for serialization and advanced configuration:

| Field | Meaning |
|---|---|
| `username` | USM security name; it must contain 1–32 bytes. |
| `auth_protocol` / `auth_password` | Both must be present or both absent. Passwords must not be empty. |
| `privacy_protocol` / `privacy_password` | Both must be present or both absent. Privacy additionally requires authentication. |
| `context_name` | Scoped-PDU context name. |
| `context_engine_id` | Optional explicit scoped-PDU engine ID. |
| `key_expansion_with_engine_id` | Java-compatible AES-192/AES-256 privacy-key expansion selection. The default is `true`. |

Prefer the constructors and builder methods over editing protocol/password option pairs manually.

Example `authPriv` configuration:

```rust,no_run
use ireasoning_snmp_sdk::{
    AuthProtocol, ClientConfig, Credentials, PrivacyProtocol,
    UsmCredentials,
};

let username = "monitor";
let auth_password = std::env::var("SNMP_AUTH_PASSWORD").unwrap();
let privacy_password = std::env::var("SNMP_PRIV_PASSWORD").unwrap();

let usm = UsmCredentials::authenticated(
    username,
    AuthProtocol::Sha256,
    auth_password,
)
.with_privacy(PrivacyProtocol::Aes256, privacy_password)
.with_context("production");

let config = ClientConfig::new(
    "router.example.com",
    Credentials::v3(usm),
);
# let _ = config;
```

The client performs discovery, password-to-key localization, engine boots/time tracking, authentication, encryption, and one bounded report-driven resynchronization automatically.

`SecurityLevel::{NoAuthNoPriv, AuthNoPriv, AuthPriv}` describes the effective security of received v3 notifications. Client security level is derived from which authentication and privacy settings are present rather than supplied as a separate parameter.

## 8. Blocking SNMP client API

`BlockingClient` owns a native client and a dedicated background Tokio runtime. It is safe to use in a conventional synchronous program and inside a process that already has another Tokio runtime.

Operations take `&mut self`, preventing concurrent or reentrant use of one handle. Create separate clients when genuinely parallel independent manager requests are needed.

### `BlockingClient::connect`

```rust
pub fn connect(config: ClientConfig) -> Result<BlockingClient>
```

Validates the configuration, resolves/connects the transport, and creates the client. For SNMPv3, authoritative-engine discovery occurs on the first operation rather than necessarily during `connect`.

### `BlockingClient::get`

```rust
pub fn get(&mut self, oids: &[Oid]) -> Result<Response>
```

- `oids` is a nonempty ordered slice of object-instance OIDs.
- One GET request contains all supplied OIDs.
- The returned varbind order is the response wire order.
- A nonzero agent error status is returned as `SdkError` with `kind == SnmpResponse`.

### `BlockingClient::get_next`

```rust
pub fn get_next(&mut self, oids: &[Oid]) -> Result<Response>
```

- `oids` is a nonempty slice of starting OIDs.
- The agent is asked for the lexicographic successor of each OID.
- This performs one GET-NEXT request; it does not automatically walk.

### `BlockingClient::set`

```rust
pub fn set(&mut self, varbinds: &[VarBind]) -> Result<Response>
```

- `varbinds` must contain at least one assignment.
- The entire slice is sent in one atomic protocol request.
- Assignment order and duplicate OIDs are preserved.
- Values must be SET-compatible variants listed in the `Value` section.
- `Counter64` is rejected under SNMPv1 before any packet is sent.
- Access, ranges, sizes, and cross-object consistency are authoritatively checked by the agent, not by local MIB metadata.

Example with multiple assignments:

```rust,no_run
use ireasoning_snmp_sdk::{
    BlockingClient, ClientConfig, Credentials, Oid, Result,
    Value, VarBind,
};

fn update(client: &mut BlockingClient) -> Result<()> {
    let assignments = vec![
        VarBind::new(
            ".1.3.6.1.2.1.1.4.0".parse::<Oid>().unwrap(),
            Value::OctetString(b"noc@example.com".to_vec()),
        ),
        VarBind::new(
            ".1.3.6.1.2.1.1.6.0".parse::<Oid>().unwrap(),
            Value::OctetString(b"rack-7".to_vec()),
        ),
    ];
    let response = client.set(&assignments)?;
    println!("agent returned {} assignments", response.pdu.varbinds.len());
    Ok(())
}
# let _ = (ClientConfig::new("localhost", Credentials::v2c("private")), update);
```

### `BulkOptions`

```rust
BulkOptions::new(
    non_repeaters: u32,
    max_repetitions: u32,
) -> BulkOptions
```

- `non_repeaters` is the number of leading OIDs for which the agent should return a single successor.
- `max_repetitions` is the requested repetition count for the remaining OIDs.
- Both must fit a nonnegative signed 32-bit wire integer.
- `BulkOptions::default()` is `{ non_repeaters: 0, max_repetitions: 50 }`.

### `BlockingClient::get_bulk`

```rust
pub fn get_bulk(
    &mut self,
    oids: &[Oid],
    options: BulkOptions,
) -> Result<Response>
```

- `oids` must be nonempty.
- `options` controls non-repeaters and repetitions.
- SNMPv1 is rejected locally.
- The response may contain fewer varbinds than requested because of agent limits, message size, or end-of-MIB values.

Example:

```rust,no_run
use ireasoning_snmp_sdk::{BlockingClient, BulkOptions, Oid, Result};

fn bulk(client: &mut BlockingClient) -> Result<()> {
    let sys_up_time = ".1.3.6.1.2.1.1.3".parse::<Oid>().unwrap();
    let if_descr = ".1.3.6.1.2.1.2.2.1.2".parse::<Oid>().unwrap();
    let response = client.get_bulk(
        &[sys_up_time, if_descr],
        BulkOptions::new(1, 20),
    )?;
    for varbind in response.pdu.varbinds {
        println!("{} = {:?}", varbind.oid, varbind.value);
    }
    Ok(())
}
```

## 9. Walk and subtree traversal

Traversal starts *after* the supplied root OID.

- A walk continues lexicographically until end-of-MIB.
- A subtree traversal stops before delivering the first OID outside `root`.
- Exception varbinds are not included in results.
- Duplicate or decreasing OIDs are rejected to prevent infinite loops.
- SNMPv1 `noSuchName` and SNMPv2 exception values terminate traversal.

### `TraversalOptions`

| Field | Default | Meaning |
|---|---:|---|
| `mode: TraversalMode` | `GetNext` | `GetNext` or `GetBulk { max_repetitions }`. |
| `max_requests: usize` | `10_000` | Maximum request/response rounds. Must be greater than zero. |
| `max_varbinds: usize` | `1_000_000` | Maximum accepted result varbinds. Must be greater than zero. |

```rust
TraversalOptions::default() -> TraversalOptions
```

Creates GET-NEXT traversal with the default safety limits.

```rust
TraversalOptions::bulk(max_repetitions: u32) -> TraversalOptions
```

Creates GET-BULK traversal with zero non-repeaters, the supplied repetition count, and the default safety limits. Bulk traversal is unavailable under SNMPv1.

### Accumulating traversal

```rust
pub fn walk(
    &mut self,
    root: &Oid,
    options: TraversalOptions,
) -> Result<TraversalResult>
```

Returns all delivered varbinds in `TraversalResult::varbinds` plus a summary. Use it when the expected result comfortably fits memory.

```rust
pub fn get_subtree(
    &mut self,
    root: &Oid,
    options: TraversalOptions,
) -> Result<TraversalResult>
```

Works like `walk`, but excludes the first result outside `root` and then stops.

### Streaming traversal

```rust
pub fn walk_with<F>(
    &mut self,
    root: &Oid,
    options: TraversalOptions,
    callback: F,
) -> Result<TraversalSummary>
where
    F: FnMut(&[VarBind]) -> ControlFlow<()>;
```

```rust
pub fn get_subtree_with<F>(
    &mut self,
    root: &Oid,
    options: TraversalOptions,
    callback: F,
) -> Result<TraversalSummary>
where
    F: FnMut(&[VarBind]) -> ControlFlow<()>;
```

Callback parameters and rules:

- The callback receives one borrowed result batch at a time.
- The batch is valid only for the callback invocation. Clone any data that must outlive it.
- Return `ControlFlow::Continue(())` to request another batch.
- Return `ControlFlow::Break(())` to stop successfully.
- Do not call another method on the same client from the callback. Reentrant native-handle use is rejected.
- A callback panic is contained and returned as `ErrorKind::Callback`.

Example streaming subtree:

```rust,no_run
use ireasoning_snmp_sdk::{
    BlockingClient, Oid, Result, TraversalOptions,
};
use std::ops::ControlFlow;

fn stream_interfaces(client: &mut BlockingClient) -> Result<()> {
    let root = ".1.3.6.1.2.1.2.2.1".parse::<Oid>().unwrap();
    let summary = client.get_subtree_with(
        &root,
        TraversalOptions::bulk(50),
        |batch| {
            for varbind in batch {
                println!("{} = {:?}", varbind.oid, varbind.value);
            }
            ControlFlow::Continue(())
        },
    )?;
    println!("{summary:?}");
    Ok(())
}
```

### Traversal result models

`TraversalResult` has `varbinds: Vec<VarBind>` and `summary: TraversalSummary`.

`TraversalSummary` has:

- `requests`: completed request count.
- `varbinds`: accepted varbind count.
- `termination: TraversalTermination`: one of `EndOfMibView`, `OutsideSubtree`, `EmptyResponse`, or `CallbackStopped`.

## 10. Async SNMP client API

`AsyncClient` uses bounded blocking jobs around the synchronous native ABI. It does not run a nested Tokio runtime in the calling async task. Cloning an `AsyncClient` shares the same underlying session, engine state, and request sequence; operations on those clones are serialized.

Cancellation is cooperative. Dropping a future does not interrupt native code mid-packet; the active attempt completes at the configured transport timeout.

### Async functions

```rust
pub async fn AsyncClient::connect(
    config: ClientConfig,
) -> Result<AsyncClient>
```

Creates the client on Tokio's blocking pool.

```rust
pub async fn get(&self, oids: &[Oid]) -> Result<Response>
pub async fn get_next(&self, oids: &[Oid]) -> Result<Response>
pub async fn set(&self, varbinds: &[VarBind]) -> Result<Response>
pub async fn get_bulk(
    &self,
    oids: &[Oid],
    options: BulkOptions,
) -> Result<Response>
```

Parameters and protocol semantics are identical to the corresponding blocking functions. Input slices are cloned before the blocking job begins.

```rust
pub async fn walk(
    &self,
    root: &Oid,
    options: TraversalOptions,
) -> Result<TraversalResult>

pub async fn get_subtree(
    &self,
    root: &Oid,
    options: TraversalOptions,
) -> Result<TraversalResult>
```

These are the accumulating async traversal forms.

```rust
pub async fn walk_with<F>(
    &self,
    root: &Oid,
    options: TraversalOptions,
    callback: F,
) -> Result<TraversalSummary>
where
    F: FnMut(&[VarBind]) -> ControlFlow<()> + Send + 'static;

pub async fn get_subtree_with<F>(
    &self,
    root: &Oid,
    options: TraversalOptions,
    callback: F,
) -> Result<TraversalSummary>
where
    F: FnMut(&[VarBind]) -> ControlFlow<()> + Send + 'static;
```

The callback runs on a blocking worker thread, not necessarily the async runtime worker that awaited the function. Captured state must therefore be `Send + 'static`. Use a channel if batches must be forwarded into another async task.

Async example:

```rust,no_run
use ireasoning_snmp_sdk::{
    AsyncClient, ClientConfig, Credentials, Oid, Result,
};

#[tokio::main]
async fn main() -> Result<()> {
    let client = AsyncClient::connect(ClientConfig::new(
        "router.example.com",
        Credentials::v2c("public"),
    ))
    .await?;

    let oids = [
        ".1.3.6.1.2.1.1.1.0".parse::<Oid>().unwrap(),
        ".1.3.6.1.2.1.1.3.0".parse::<Oid>().unwrap(),
    ];
    let response = client.get(&oids).await?;
    println!("{:?}", response.pdu.varbinds);
    Ok(())
}
```

## 11. Trap and INFORM receiver configuration

The receiver listens on UDP and supports SNMPv1 traps, SNMPv2c traps and INFORMs, and SNMPv3 traps and INFORMs. Valid INFORMs are acknowledged before their `Notification` is returned to the application.

### `TrapReceiverConfig::new`

```rust
TrapReceiverConfig::new(
    bind_address: IpAddr,
    port: u16,
) -> TrapReceiverConfig
```

Creates receiver configuration with a 128 KiB message limit, 256 cached engine entries, no community restriction, no USM users, no authoritative receiver engine, and a 5,000 ms default receive timeout.

Use port `0` when tests need an automatically assigned local port. Port `162` may require elevated privileges on Unix-like systems.

Configuration fields are:

| Field | Meaning |
|---|---|
| `bind_address: IpAddr` | Local IPv4 or IPv6 interface address. Use an unspecified address for all interfaces. |
| `port: u16` | Local UDP port. |
| `max_message_size: usize` | Maximum datagram size; it must be at least 484 bytes. |
| `max_engine_cache_entries: usize` | Bound for SNMPv3 engine timeliness and localized-key caches; it must be greater than zero. |
| `accepted_communities: Vec<Vec<u8>>` | Optional exact community allowlist. Empty means accept every v1/v2c community. Values are compared in constant time and redacted from output. |
| `users: Vec<ReceiverUsmUser>` | Configured SNMPv3 users. Multiple users and protocol combinations are allowed. |
| `authoritative_engine: Option<AuthoritativeEngine>` | Receiver engine used for SNMPv3 INFORM discovery, REPORTs, timeliness, and acknowledgements. |
| `receive_timeout_ms: u64` | Bounded wait used by `recv`; use a value greater than zero. |

```rust
config.socket_addr() -> SocketAddr
```

Combines `bind_address` and `port`. Before binding, a zero port remains zero; call `receiver.local_addr()` after binding to discover the assigned port.

### `ReceiverUsmUser`

```rust
pub struct ReceiverUsmUser {
    pub credentials: UsmCredentials,
    pub authoritative_engine_id: Option<Vec<u8>>,
}
```

- `credentials` supplies username, authentication, privacy, and Java-compatible key expansion.
- `authoritative_engine_id` optionally restricts this user entry to one sender engine. Leave it `None` to accept matching messages from any valid engine, subject to the receiver's timeliness tracking.

### `AuthoritativeEngine`

```rust
pub struct AuthoritativeEngine {
    pub engine_id: Vec<u8>,
    pub engine_boots: u32,
}
```

- `engine_id` must contain 5–32 octets and should be stable across restarts.
- `engine_boots` must not exceed `i32::MAX`. Persist it and increment it before each receiver restart.
- Engine time is derived from elapsed process time after binding.

The SDK does not persist receiver engine state itself; applications or the `snmptrapd` CLI are responsible for durable engine ID and boots storage.

SNMPv3 receiver example:

```rust,no_run
use ireasoning_snmp_sdk::{
    AuthProtocol, AuthoritativeEngine, PrivacyProtocol,
    ReceiverUsmUser, TrapReceiverConfig, UsmCredentials,
};
use std::net::{IpAddr, Ipv4Addr};

let user = ReceiverUsmUser {
    credentials: UsmCredentials::authenticated(
        "trap-user",
        AuthProtocol::Sha256,
        std::env::var("SNMP_AUTH_PASSWORD").unwrap(),
    )
    .with_privacy(
        PrivacyProtocol::Aes128,
        std::env::var("SNMP_PRIV_PASSWORD").unwrap(),
    ),
    authoritative_engine_id: None,
};

let mut config = TrapReceiverConfig::new(
    IpAddr::V4(Ipv4Addr::UNSPECIFIED),
    9162,
);
config.users.push(user);
config.authoritative_engine = Some(AuthoritativeEngine {
    engine_id: vec![0x80, 0x00, 0x3b, 0x29, 1, 2, 3, 4, 5],
    engine_boots: 1,
});
```

## 12. Blocking trap receiver API

### `BlockingTrapReceiver::bind`

```rust
pub fn bind(
    config: TrapReceiverConfig,
) -> Result<BlockingTrapReceiver>
```

Validates configuration, binds UDP, and starts the receiver's dedicated runtime thread.

### `BlockingTrapReceiver::local_addr`

```rust
pub fn local_addr(&self) -> Result<SocketAddr>
```

Returns the actual bound address. This is especially useful when configured with port `0`.

### `BlockingTrapReceiver::recv`

```rust
pub fn recv(&mut self) -> Result<Notification>
```

Waits up to `TrapReceiverConfig::receive_timeout_ms` for one valid notification. A timeout produces `ErrorKind::Timeout`. A malformed or unauthenticated datagram may produce a protocol/USM error for this single receive call.

### `BlockingTrapReceiver::recv_timeout`

```rust
pub fn recv_timeout(
    &mut self,
    timeout: Duration,
) -> Result<Notification>
```

Uses an explicit timeout instead of the configured default. `timeout` must be greater than zero; sub-millisecond positive durations are rounded up to one millisecond at the ABI boundary.

### `BlockingTrapReceiver::run`

```rust
pub fn run<F>(&mut self, callback: F) -> Result<()>
where
    F: FnMut(Notification) -> ControlFlow<()>;
```

Repeatedly receives notifications. Timeout, malformed notification, and USM rejection errors are treated as recoverable and the loop continues. Transport, configuration, ABI, and internal failures stop the loop.

The callback owns each `Notification`. Return `Continue(())` to keep receiving or `Break(())` to stop successfully.

Example:

```rust,no_run
use ireasoning_snmp_sdk::{
    BlockingTrapReceiver, Result, TrapReceiverConfig,
};
use std::net::{IpAddr, Ipv4Addr};
use std::ops::ControlFlow;

fn main() -> Result<()> {
    let mut config = TrapReceiverConfig::new(
        IpAddr::V4(Ipv4Addr::UNSPECIFIED),
        9162,
    );
    config.accepted_communities.push(b"public".to_vec());

    let mut receiver = BlockingTrapReceiver::bind(config)?;
    println!("listening on {}", receiver.local_addr()?);

    receiver.run(|notification| {
        println!(
            "{} {:?} oid={:?} uptime={:?}",
            notification.source,
            notification.kind,
            notification.notification_oid(),
            notification.uptime(),
        );
        ControlFlow::Continue(())
    })
}
```

## 13. Async trap receiver API

`AsyncTrapReceiver` is cloneable. Clones share one underlying socket and serialize receive operations. Avoid awaiting multiple concurrent `recv` calls on clones when notification ownership order matters.

```rust
pub async fn AsyncTrapReceiver::bind(
    config: TrapReceiverConfig,
) -> Result<AsyncTrapReceiver>
```

Binds on Tokio's blocking pool.

```rust
pub fn local_addr(&self) -> Result<SocketAddr>
```

Returns the bound address synchronously; it performs no network wait.

```rust
pub async fn recv(&self) -> Result<Notification>
```

Receives with the configured bounded timeout.

```rust
pub async fn recv_timeout(
    &self,
    timeout: Duration,
) -> Result<Notification>
```

Receives with an explicit bounded timeout.

```rust
pub async fn run<F>(&self, callback: F) -> Result<()>
where
    F: FnMut(Notification) -> ControlFlow<()> + Send + 'static;
```

Runs the recoverable receive loop. The callback executes in the async task after each blocking receive job completes. It must be `Send + 'static` because the async future may move between runtime threads.

Cancellation is bounded by the active receive timeout.

## 14. Notification models

`Notification` fields are:

| Field | Meaning |
|---|---|
| `source: SocketAddr` | Sender address. |
| `local_address: SocketAddr` | Receiver address that accepted the datagram. |
| `version: SnmpVersion` | `V1`, `V2c`, or `V3`. |
| `kind: NotificationKind` | `V1Trap`, `V2Trap`, or `Inform`. |
| `security: NotificationSecurity` | Redacted community marker or USM identity/security metadata. |
| `context_engine_id: Vec<u8>` | SNMPv3 scoped context engine ID; empty for community notifications. |
| `context_name: Vec<u8>` | Raw context-name octets; empty for community notifications. |
| `request_id: Option<i32>` | Request ID for v2/v3 notifications; absent for the distinct v1 Trap-PDU. |
| `varbinds: Vec<VarBind>` | Notification variable bindings. |
| `v1_trap: Option<V1Trap>` | SNMPv1-specific Trap-PDU fields. |

`NotificationSecurity` is either:

- `Community`, which intentionally does not expose the received community; or
- `Usm { username, authoritative_engine_id, security_level }`, where `security_level` is `NoAuthNoPriv`, `AuthNoPriv`, or `AuthPriv`.

```rust
notification.notification_oid() -> Option<Oid>
```

Returns the normalized notification OID:

- For v2/v3, it reads `snmpTrapOID.0`.
- For a generic v1 trap, it returns the corresponding standard notification OID.
- For an enterprise-specific v1 trap, it returns `enterprise.0.specific-trap`.
- It returns `None` when the notification cannot be normalized.

```rust
notification.uptime() -> Option<u32>
```

Returns v1 Trap-PDU timestamp or the v2/v3 `sysUpTime.0` TimeTicks value.

`V1Trap` contains `enterprise`, four-octet `agent_address`, `generic_trap`, `specific_trap`, and `timestamp`. `GenericTrap` distinguishes the six standard generic types, `EnterpriseSpecific`, and preserved `Unknown(i32)` values.

## 15. MIB parsing concepts

Use `MibParser` for a standalone parse and `MibRegistry` when imports, multiple modules, symbolic resolution, formatting, merged trees, or module lifecycle are needed.

Every registry is independent. Loading or clearing one registry has no effect on another registry in the same process.

MIB input encoding is detected as follows:

- UTF-8 BOM selects UTF-8.
- UTF-16LE/UTF-16BE BOMs are recognized.
- Java-compatible zero-byte patterns detect unmarked UTF-16.
- Other unmarked bytes are interpreted as ISO-8859-1.

### `ParseOptions`

| Field | Default | Meaning |
|---|---:|---|
| `strict: bool` | `true` | Strict mode returns typed errors for unresolved/cyclic imports and required parse failures. Lenient mode keeps placeholders and diagnostics when possible. |
| `depth: ParseDepth` | `Full` | `ModuleName`, `Imports`, or `Full`. |
| `resolve_syntax: bool` | `false` | Resolve derived/textual-convention syntax into basic syntax metadata where possible. |

`ParseDepth::ModuleName` parses only enough to identify the module. `Imports` also records imports/exports. `Full` builds metadata, textual conventions, traps, and the OID tree.

## 16. `MibParser` API

### Construction

```rust
MibParser::new(options: ParseOptions) -> MibParser
MibParser::default() -> MibParser
```

`default` uses `ParseOptions::default()`.

```rust
parser.options() -> ParseOptions
```

Returns the parser's copied options.

### Parsing functions

```rust
parser.parse_path(
    path: impl AsRef<Path>,
) -> Result<MibModule>
```

Reads and parses one filesystem path. `MibError` details are mapped to `ErrorKind::Mib`.

```rust
parser.parse_reader(
    source_name: &str,
    reader: impl Read,
) -> Result<MibModule>
```

- `source_name` is recorded in diagnostics and the returned module.
- `reader` is consumed until EOF and may be a file, cursor, socket-derived buffer, or other synchronous reader.

```rust
parser.parse_bytes(
    source_name: &str,
    bytes: &[u8],
) -> Result<MibModule>
```

Parses an in-memory byte slice with encoding detection.

```rust
MibParser::module_name(
    source_name: &str,
    bytes: &[u8],
) -> Result<String>
```

Statically parses only the module header and returns the `DEFINITIONS` name. It is useful for indexing a MIB directory without a full parse.

Example:

```rust,no_run
use ireasoning_snmp_sdk::{
    MibParser, ParseDepth, ParseOptions, Result,
};

fn main() -> Result<()> {
    let parser = MibParser::new(ParseOptions {
        strict: false,
        depth: ParseDepth::Full,
        resolve_syntax: true,
    });
    let module = parser.parse_path("mibs/IF-MIB")?;
    println!("{}: {} nodes", module.name, module.tree.nodes.len());
    for diagnostic in module.diagnostics {
        eprintln!(
            "{}:{}:{}: {:?}: {}",
            diagnostic.source,
            diagnostic.line,
            diagnostic.column,
            diagnostic.severity,
            diagnostic.message,
        );
    }
    Ok(())
}
```

## 17. `MibRegistry` API

### Construction and directories

```rust
MibRegistry::new() -> Result<MibRegistry>
```

Creates an empty independent native registry. Prefer this fallible constructor.

`MibRegistry::default()` also exists, but it panics if the native ABI cannot be loaded or is incompatible; it is mainly convenient after SDK deployment has already been validated.

```rust
registry.add_search_path(
    path: impl AsRef<Path>,
) -> Result<()>
```

Canonicalizes and adds an existing file or directory used to locate imported modules. Directories are searched recursively, hidden entries are skipped, and candidate module names are read from their headers.

```rust
registry.search_paths() -> Result<Vec<PathBuf>>
```

Returns owned copies of the currently configured canonical search paths.

### Loading modules

```rust
registry.load_path(
    path: impl AsRef<Path>,
    options: ParseOptions,
) -> Result<ModuleId>
```

Loads a filesystem MIB and resolves imports by loaded module name, the source file directory, and configured search paths. Loading the same canonical path or module name returns the existing stable `ModuleId`.

```rust
registry.load_reader(
    source_name: &str,
    reader: impl Read,
    options: ParseOptions,
) -> Result<ModuleId>
```

Reads an arbitrary synchronous reader and loads the resulting module. `source_name` is used for metadata and diagnostics.

```rust
registry.load_bytes(
    source_name: &str,
    bytes: &[u8],
    options: ParseOptions,
) -> Result<ModuleId>
```

Loads in-memory bytes. Import resolution uses already loaded modules and configured paths; an in-memory source has no independently discoverable filesystem directory unless `source_name` names a usable path.

```rust
registry.load_mib2() -> Result<ModuleId>
```

Loads the embedded RFC1213 MIB using lenient full parsing with syntax resolution. Repeated calls return the existing module ID.

### Inspecting loaded state

```rust
registry.is_loaded_path(
    path: impl AsRef<Path>,
) -> Result<bool>
```

Returns whether the canonical path is loaded. A path that cannot be canonicalized returns `false` rather than a MIB error.

```rust
registry.is_any_loaded() -> Result<bool>
```

Returns whether at least one live module is loaded.

```rust
registry.module_id(name: &str) -> Result<Option<ModuleId>>
```

Looks up a module name case-insensitively.

```rust
registry.module(id: ModuleId) -> Result<MibModule>
```

Returns an owned module copy. An unloaded, stale, or out-of-range ID returns a MIB error.

```rust
registry.module_by_name(
    name: &str,
) -> Result<Option<MibModule>>
```

Combines name lookup and owned module retrieval.

```rust
registry.modules() -> Result<Vec<ModuleSummary>>
```

Returns deterministic summaries for live modules. Each summary contains `id`, `name`, `source`, `metadata`, `imports`, and module display hints.

```rust
registry.display_hints() -> Result<BTreeMap<String, String>>
```

Returns the merged textual-convention display-hint map for loaded modules.

### Removing state

```rust
registry.unload_path(
    path: impl AsRef<Path>,
) -> Result<bool>
```

Unloads the module associated with a canonical path. Returns `true` if a live module was removed.

```rust
registry.unload(id: ModuleId) -> Result<bool>
```

Unloads a module by stable handle. Existing copies of `MibModule` remain usable, but the ID no longer addresses a live registry module.

```rust
registry.clear() -> Result<()>
```

Removes loaded modules, indexes, loading state, and merged display hints. Configured search paths remain available for subsequent loads.

### Symbolic OID resolution

```rust
registry.resolve_oid(value: &str) -> Result<Oid>
```

Accepts:

- numeric OIDs such as `.1.3.6.1.2.1.1.3.0`;
- unqualified symbols such as `sysUpTime.0` and `ifDescr.2`; or
- qualified symbols such as `IF-MIB::ifDescr.2`.

Numeric suffix arcs are appended to the resolved base symbol. Ambiguous unqualified names return a MIB error rather than selecting an arbitrary module.

```rust
registry.resolve_oid_in_module(
    module: &str,
    value: &str,
) -> Result<Oid>
```

Resolves with a preferred module. Module-local definitions and explicit imports are considered before global candidates. `module` must identify a loaded module.

### Formatting

```rust
registry.format_oid(
    oid: &Oid,
    full_name: bool,
) -> Result<Option<String>>
```

Uses the longest loaded OID prefix. If `full_name` is `false`, a result resembles `sysUpTime.0`; if `true`, it resembles `RFC1213-MIB::sysUpTime.0`. Returns `None` when no loaded node prefixes the OID.

```rust
registry.format_varbind(
    varbind: &VarBind,
    numeric_oid: bool,
) -> Result<FormattedVarBind>
```

Returns `{ name, value }`:

- `numeric_oid == true` forces numeric `name` output.
- Otherwise the best symbolic name is used, falling back to numeric.
- Integer enum metadata formats values such as `up(1)` when available.
- Other values use their normal SNMP representation.

### Trees and lookup

```rust
registry.merged_tree() -> Result<MibTree>
```

Merges every loaded module into a new owned OID tree.

```rust
registry.merge_modules(
    ids: &[ModuleId],
) -> Result<MibTree>
```

Merges only the listed live module IDs. Invalid IDs are skipped by the merge operation.

```rust
registry.lookup_oid(
    oid: &Oid,
) -> Result<Option<MibLookup>>
```

Returns the loaded module and node with the longest matching OID prefix. The returned `MibLookup` owns `module: String` and `node: MibNode`.

```rust
registry.lookup_name(
    name: &str,
) -> Result<Vec<MibLookup>>
```

Returns every case-insensitive name match. `MODULE::symbol` restricts the search to one module. Multiple results are retained instead of being treated as resolution ambiguity.

```rust
registry.lookup_trap_oid(
    oid: &Oid,
) -> Result<Vec<TrapLookup>>
```

Returns matching SMIv1 and SMIv2 trap definitions in deterministic module/name order. SMIv1 enterprise-specific traps are normalized as `enterprise.0.number`.

Complete registry example:

```rust,no_run
use ireasoning_snmp_sdk::{
    MibRegistry, ParseOptions, Result, Value, VarBind,
};

fn main() -> Result<()> {
    let mut registry = MibRegistry::new()?;
    registry.add_search_path("mibs")?;
    registry.load_mib2()?;
    registry.load_path(
        "mibs/IF-MIB",
        ParseOptions {
            strict: false,
            resolve_syntax: true,
            ..ParseOptions::default()
        },
    )?;

    let oid = registry.resolve_oid("IF-MIB::ifAdminStatus.1")?;
    let varbind = VarBind::new(oid.clone(), Value::Integer(1));
    let formatted = registry.format_varbind(&varbind, false)?;
    println!("{} = {}", formatted.name, formatted.value);

    if let Some(found) = registry.lookup_oid(&oid)? {
        println!("module={} node={}", found.module, found.node.name);
    }
    Ok(())
}
```

## 18. MIB result models

All MIB models are owned and Serde serializable/deserializable. They can outlive the registry call that produced them.

### Handles and trees

- `ModuleId(pub usize)` is a registry-local stable module handle. Do not use an ID with another registry.
- `NodeId(pub usize)` indexes `MibTree::nodes` and is meaningful only within that tree.

`MibTree` has `root: NodeId` and `nodes: Vec<MibNode>`.

```rust
tree.node(id: NodeId) -> Option<&MibNode>
```

Returns a node by arena ID.

```rust
tree.find_name(name: &str) -> Option<NodeId>
```

Returns the first case-insensitive name match in tree order.

```rust
tree.find_oid(oid: &Oid) -> Option<NodeId>
```

Returns the exact OID match. For longest-prefix behavior across loaded modules, use `MibRegistry::lookup_oid`.

### `MibModule`

| Field | Meaning |
|---|---|
| `name` | Module definition name. |
| `source` | File path or caller-supplied source name. |
| `metadata` | `MODULE-IDENTITY` metadata and revisions. |
| `imports` / `exports` | Parsed import/export declarations. |
| `tree` | Owned arena OID tree. |
| `traps` | SMIv1 `TRAP-TYPE` and SMIv2 notification metadata. |
| `textual_conventions` | Name-to-`Syntax` map. |
| `display_hints` | Parsed display hints. |
| `diagnostics` | Typed warnings/errors retained by lenient parsing. |

`ModuleMetadata` contains optional `identity`, `last_updated`, `organization`, `contact_info`, and `description`, plus `revisions: Vec<Revision>`. A `Revision` contains `timestamp` and optional `description`.

An `Import` contains the imported `module` name and its `symbols`.

### `MibNode`

Important node fields are:

| Field | Meaning |
|---|---|
| `id`, `parent`, `children` | Arena traversal links. |
| `name`, `full_name`, `oid`, `module` | Symbolic/numeric identity and owning module. |
| `node_type: NodeType` | `Root`, `ObjectType`, `ObjectIdentity`, `Notification`, `NotificationGroup`, `ModuleIdentity`, `Compliance`, `ObjectGroup`, `Other`, or `Unknown`. |
| `description`, `access`, `status`, `units`, `default_value` | Parsed SMI clauses. |
| `syntax` | Optional resolved/derived syntax. |
| `indexes`, `table_indexes` | Entry/table index definitions, including `implied`. |
| `augments`, `augments_table` | Augmentation metadata. |
| `objects`, `object_oids` | Notification/group object references and resolved OIDs. |
| `is_table` | Table classification. |
| `row_status_oid`, `entry_status` | RowStatus-related metadata. |

`Syntax` contains `type_name`, optional `derived_type`, raw declaration, size/range text, enum `values`, optional `display_hint`, and optional `BaseSyntax`. Basic syntax values include `ObjectIdentifier`, `OctetString`, `Integer`, `Unsigned32`, `Gauge32`, `Counter32`, `Counter64`, `TimeTicks`, `IpAddress`, `Bits`, and `Opaque`.

An `Index` contains `name` and the `implied` flag.

### Traps and diagnostics

`Trap` contains `version`, `name`, enterprise name/OID, optional v1 number, variable names/OIDs, description, and optional tree node. `TrapVersion` is `V1` or `V2`.

`Diagnostic` contains:

- `severity: Severity`: `Warning` or `Error`;
- `code: DiagnosticCode`: `Syntax`, `InvalidHeader`, `InvalidEncoding`, `UnresolvedImport`, `UnresolvedSymbol`, `AmbiguousSymbol`, `DuplicateOid`, `ImportCycle`, or `Io`;
- `message`, `source`, optional module/imported-module names; and
- one-based `line` and `column`.

`ModuleSummary`, `MibLookup`, `TrapLookup`, and `FormattedVarBind` are the compact owned result types returned by registry inspection and formatting methods. `RegistrySnapshot` is a serializable aggregation model containing search paths, module summaries, and display hints.

`RegistryFormatRequest` is the serializable `{ varbind: VarBind, numeric_oid: bool }` model used by schema-level integrations. Safe Rust applications normally call `MibRegistry::format_varbind` directly instead of constructing it.

## 19. Using MIB names with SNMP operations

The SNMP client accepts numeric `Oid` values. Resolve names through a registry before sending them:

```rust,no_run
use ireasoning_snmp_sdk::{
    BlockingClient, ClientConfig, Credentials, MibRegistry,
    Result,
};

fn main() -> Result<()> {
    let mut mibs = MibRegistry::new()?;
    mibs.load_mib2()?;

    let oid = mibs.resolve_oid("sysUpTime.0")?;
    let mut client = BlockingClient::connect(ClientConfig::new(
        "router.example.com",
        Credentials::v2c("public"),
    ))?;
    let response = client.get(&[oid])?;

    for varbind in response.pdu.varbinds {
        let formatted = mibs.format_varbind(&varbind, false)?;
        println!("{} = {}", formatted.name, formatted.value);
    }
    Ok(())
}
```

For an OID-valued SET input, resolve the value OID separately and wrap it in `Value::ObjectIdentifier`.

## 20. Thread safety, ownership, and callbacks

- Native objects are represented internally by synchronized, type-checked handles.
- A `BlockingClient` or `BlockingTrapReceiver` operation requires mutable access.
- `AsyncClient` and `AsyncTrapReceiver` clones share and serialize access to the same native object.
- Create separate clients or receivers when independent parallel state is required.
- Each `MibRegistry` owns independent module state.
- Result models are owned Rust values; they do not borrow native memory.
- Traversal callback slices are temporary and must not escape the callback without cloning.
- Do not re-enter the same client from its traversal callback.
- Native handle cleanup occurs automatically in `Drop`.
- Completed network operations are stored in one owned native result payload, copied once, and freed. Buffer sizing cannot repeat SET or another network operation.

## 21. Security guidance

- Obtain communities and passwords from a secret manager or protected environment, not source literals.
- Credential-bearing types redact `Debug` output and zero owned secret fields when dropped.
- Temporary serialized credential buffers in the safe façade are zeroized.
- Native errors never include communities, passwords, localized keys, or salts.
- Do not serialize credential configuration into logs or application telemetry.
- An empty trap community allowlist intentionally accepts all v1/v2c communities. Configure an allowlist when community filtering is required.
- Persist only SNMPv3 authoritative receiver engine ID and boots state. Do not store credentials in the engine-state file.
- Treat unauthenticated traps as untrusted input even if their community passes a filter.

## 22. Deployment checklist

1. Use the SDK archive for the application's exact target triple.
2. Select exactly one of static or dynamic linkage.
3. Call `build_info()` during startup diagnostics if early compatibility reporting is desired.
4. Ensure dynamic libraries are installed where the platform loader can find them.
5. Set realistic `timeout_ms`, `retries`, packet limits, and traversal limits.
6. Store SNMP credentials outside source and logs.
7. For SNMPv3 INFORM reception, persist a stable authoritative engine ID and increment engine boots before binding after each restart.
8. Use nonprivileged receiver ports unless deployment explicitly grants access to UDP 162.
9. Prefer streaming traversal for large device trees.
10. Load MIBs before resolving or formatting symbolic OIDs.

## 23. Troubleshooting

### `IncompatibleAbi` at startup

The Rust bindings and native library came from different SDK major/schema versions. Replace all SDK files as one unit; do not mix archives.

### Native library not found while linking

Confirm the target archive and linkage feature, or set `IREASONING_SNMP_SDK_DIR` to the directory containing the native files.

### Dynamic library not found at runtime

The application linked successfully, but the operating-system loader cannot locate the `.so`, `.dylib`, or `.dll`. Install it beside the executable or configure the platform's loader path/rpath.

### GET-BULK fails with SNMPv1

GET-BULK is an SNMPv2 operation. Use `Credentials::v2c` or SNMPv3.

### Symbolic OID is not found

Load the defining MIB and its imports into the same `MibRegistry`, configure search paths, and use `MODULE::symbol` when duplicate names exist.

### Trap receiver repeatedly times out

Verify the bound address/port using `local_addr`, firewall and routing rules, sender destination, and `receive_timeout_ms`. A timeout is normal when no notification arrives during one bounded receive window.

### SNMPv3 authentication/privacy error

Verify username, protocol selection, password, sender engine restriction, authoritative receiver engine configuration for INFORMs, engine boots persistence, clock/timeliness behavior, and Java-compatible AES key-expansion selection.
