Skip to content

Commit fab5f54

Browse files
Greg Lambersonlamco-office
authored andcommitted
feat(error): add ErrorClassification trait for triage-friendly Error<Kind> categories
Introduces ironrdp_error::{ErrorCategory, ErrorClassification} so consumers can route errors by coarse category (Protocol, DataCorruption, InternalBug, Unknown) without inspecting Display strings. Adds a convenience Error<Kind>::classify() method that is available when Kind: ErrorClassification. Implements the trait for the two foundational Kind types in ironrdp-core: DecodeErrorKind classifies all structured variants as Protocol (peer-driven wire-format failures) and Other as Unknown; EncodeErrorKind classifies all structured variants as InternalBug (we miscalculated something in our own serialisation path) and Other as Unknown. ironrdp-core re-exports the trait and enum so consumers can pull them via the same crate they already use for DecodeError/EncodeError type aliases. Primary consumer is the structured-fuzzing oracle code in ironrdp-fuzzing tracked under #1120: categories let an oracle distinguish "decoder correctly rejected malformed input" from "decoder hit an internal invariant violation" without inspecting Display strings. The DataCorruption category is defined now and reserved for the deeper-validation variants that other *ErrorKind enums in the workspace will adopt in follow-on PRs. Refs #1257, #1120.
1 parent 2e2699d commit fab5f54

4 files changed

Lines changed: 113 additions & 0 deletions

File tree

crates/ironrdp-core/src/decode.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
use alloc::string::String;
33
use core::fmt;
44

5+
use ironrdp_error::{ErrorCategory, ErrorClassification};
6+
57
use crate::{
68
InvalidFieldErr, NotEnoughBytesErr, OtherErr, ReadCursor, UnexpectedMessageTypeErr, UnsupportedValueErr,
79
UnsupportedVersionErr,
@@ -66,6 +68,23 @@ pub enum DecodeErrorKind {
6668
#[cfg(feature = "std")]
6769
impl core::error::Error for DecodeErrorKind {}
6870

71+
impl ErrorClassification for DecodeErrorKind {
72+
fn classify(&self) -> ErrorCategory {
73+
// All structured variants are peer-driven: the input came from the
74+
// wire and failed to conform to the spec at some level. `Other` is
75+
// a free-form catch-all that cannot be classified without inspecting
76+
// the description, so it stays `Unknown`.
77+
match self {
78+
Self::NotEnoughBytes { .. }
79+
| Self::InvalidField { .. }
80+
| Self::UnexpectedMessageType { .. }
81+
| Self::UnsupportedVersion { .. }
82+
| Self::UnsupportedValue { .. } => ErrorCategory::Protocol,
83+
Self::Other { .. } => ErrorCategory::Unknown,
84+
}
85+
}
86+
}
87+
6988
impl fmt::Display for DecodeErrorKind {
7089
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7190
match self {

crates/ironrdp-core/src/encode.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use alloc::string::String;
44
use alloc::{vec, vec::Vec};
55
use core::fmt;
66

7+
use ironrdp_error::{ErrorCategory, ErrorClassification};
8+
79
#[cfg(feature = "alloc")]
810
use crate::WriteBuf;
911
use crate::{
@@ -70,6 +72,24 @@ pub enum EncodeErrorKind {
7072
#[cfg(feature = "std")]
7173
impl core::error::Error for EncodeErrorKind {}
7274

75+
impl ErrorClassification for EncodeErrorKind {
76+
fn classify(&self) -> ErrorCategory {
77+
// Encode errors are produced by our own code while serialising data
78+
// we constructed; every structured variant indicates we hit an
79+
// internal invariant violation (under-sized buffer, attempting to
80+
// write an out-of-range value, etc.). `Other` is a free-form
81+
// catch-all and stays `Unknown`.
82+
match self {
83+
Self::NotEnoughBytes { .. }
84+
| Self::InvalidField { .. }
85+
| Self::UnexpectedMessageType { .. }
86+
| Self::UnsupportedVersion { .. }
87+
| Self::UnsupportedValue { .. } => ErrorCategory::InternalBug,
88+
Self::Other { .. } => ErrorCategory::Unknown,
89+
}
90+
}
91+
}
92+
7393
impl fmt::Display for EncodeErrorKind {
7494
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7595
match self {

crates/ironrdp-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ mod write_buf;
2222

2323
// Flat API hierarchy of common traits and types
2424

25+
pub use ironrdp_error::{ErrorCategory, ErrorClassification};
26+
2527
pub use self::as_any::*;
2628
pub use self::cursor::*;
2729
pub use self::decode::*;

crates/ironrdp-error/src/lib.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,65 @@ struct ErrorMeta {
2626
source: Option<Box<dyn Source>>,
2727
}
2828

29+
/// Coarse category assigned to a `*ErrorKind` variant by [`ErrorClassification`],
30+
/// used to drive triage logic that does not depend on the specific kind shape.
31+
///
32+
/// The intended primary consumer is the structured-fuzzing oracle code in
33+
/// `ironrdp-fuzzing`, which uses the category to distinguish "the decoder
34+
/// correctly rejected malformed input" from "the decoder hit an internal
35+
/// invariant violation" without parsing `Display` strings.
36+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37+
#[non_exhaustive]
38+
pub enum ErrorCategory {
39+
/// Wire-protocol violation by the peer: the input does not conform to the
40+
/// specification (invalid field, unsupported version, unexpected message
41+
/// type, premature end of input, etc.).
42+
///
43+
/// For a fuzz oracle, this is the expected outcome when fuzzing decoders
44+
/// with arbitrary bytes: the decoder caught a malformed input.
45+
Protocol,
46+
47+
/// Logical inconsistency in otherwise spec-conformant data: the peer
48+
/// sent something whose individual fields parse but whose meaning is
49+
/// internally contradictory (e.g. mismatched lengths, impossible
50+
/// references, failed integrity checks).
51+
///
52+
/// For a fuzz oracle, this is also an expected rejection; treated
53+
/// separately from [`ErrorCategory::Protocol`] because the diagnostic
54+
/// implications differ (deeper validation logic was reached before the
55+
/// rejection).
56+
DataCorruption,
57+
58+
/// Internal invariant violation in our own code: an unexpected state
59+
/// was reached that should not occur for any valid or invalid input.
60+
///
61+
/// For a fuzz oracle, this is the bug signal: the kind of failure that
62+
/// should be minimized to a crasher and reported.
63+
InternalBug,
64+
65+
/// Classification not specified for this variant. Either the `Kind`
66+
/// type has not yet been audited to assign categories to all its
67+
/// variants, or the variant is intentionally left uncategorized
68+
/// pending design decisions.
69+
Unknown,
70+
}
71+
72+
/// Assigns an [`ErrorCategory`] to each variant of a `*ErrorKind` enum.
73+
///
74+
/// Implement on the `Kind` type carried by [`Error<Kind>`]. Once implemented,
75+
/// [`Error::classify`] becomes available on `Error<Kind>` and forwards to this
76+
/// trait's [`classify`](Self::classify) method.
77+
///
78+
/// Per the [`ErrorCategory`] documentation, the primary consumer is the
79+
/// structured-fuzzing oracle code. Other consumers may include diagnostic
80+
/// logging that routes errors to different sinks based on category, or
81+
/// integration tests that assert on the expected category of an induced
82+
/// failure without depending on the specific variant shape.
83+
pub trait ErrorClassification {
84+
/// Returns the category for this variant.
85+
fn classify(&self) -> ErrorCategory;
86+
}
87+
2988
/// A typed error wrapper carrying a `Kind` discriminant plus diagnostic metadata.
3089
///
3190
/// # `no_alloc` platforms
@@ -157,6 +216,19 @@ impl<Kind> Error<Kind> {
157216
}
158217
}
159218

219+
impl<Kind> Error<Kind>
220+
where
221+
Kind: ErrorClassification,
222+
{
223+
/// Returns the [`ErrorCategory`] of this error, forwarded from the
224+
/// inner `Kind` via its [`ErrorClassification`] impl.
225+
///
226+
/// Only available when `Kind` implements [`ErrorClassification`].
227+
pub fn classify(&self) -> ErrorCategory {
228+
self.kind.classify()
229+
}
230+
}
231+
160232
impl<Kind> fmt::Display for Error<Kind>
161233
where
162234
Kind: fmt::Display,

0 commit comments

Comments
 (0)