Skip to content

Commit 1d90327

Browse files
author
Greg Lamberson
committed
feat(error): record optional byte offset and field context on Error<Kind>
Adds two composable builder methods to ironrdp-error::Error<Kind> that record optional positional metadata: Error::at_offset records a byte offset in the input stream, and Error::in_field records a symbolic field or sub-PDU name. Both are optional, both compose, and both are surfaced in Display output when present. Position lives as a fourth field on ErrorMeta (alongside context, location, source) so it carries zero size cost on Result-Ok paths and is available only when the alloc feature is enabled. Refs #1257, #1120. Depends on #1269.
1 parent 6c00957 commit 1d90327

1 file changed

Lines changed: 117 additions & 4 deletions

File tree

  • crates/ironrdp-error/src

crates/ironrdp-error/src/lib.rs

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,39 @@ pub trait Source: core::error::Error + Send + Sync + 'static {}
1313

1414
impl<T> Source for T where T: core::error::Error + Send + Sync + 'static {}
1515

16+
/// Optional positional metadata attached to an [`Error`] when the failure
17+
/// occurred at a known byte offset within a wire-format input stream, and/or
18+
/// while processing a known symbolic field.
19+
///
20+
/// Populated via [`Error::at_offset`] and [`Error::in_field`]. These builders
21+
/// compose: both may be set independently on the same error. When either is
22+
/// present, the position is rendered in [`Error`]'s `Display` output.
23+
///
24+
/// Intended primarily for decode and encode errors where the byte offset and
25+
/// the field being processed materially aid triage, particularly for
26+
/// fuzz-crash analysis where the variant kind alone does not narrow the
27+
/// failure point down enough.
28+
///
29+
/// Available only when the `alloc` feature is enabled, since it lives in
30+
/// [`Error`]'s heap-allocated metadata.
31+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32+
#[non_exhaustive]
33+
pub struct ErrorPosition {
34+
/// Byte offset in the input stream where the error was detected.
35+
pub offset: Option<usize>,
36+
/// Symbolic name of the field or sub-PDU being processed when the error
37+
/// occurred. Dotted notation may be used for nested PDU navigation,
38+
/// e.g. `"ServerSecurityData.serverRandom"`.
39+
pub field: Option<&'static str>,
40+
}
41+
42+
impl ErrorPosition {
43+
/// Returns `true` if at least one of `offset` or `field` is set.
44+
pub const fn is_set(&self) -> bool {
45+
self.offset.is_some() || self.field.is_some()
46+
}
47+
}
48+
1649
/// Diagnostic metadata stored behind a [`Box`] so that `Error<Kind>` stays small.
1750
///
1851
/// All fields here are purely for display and error-chain traversal; none are
@@ -24,6 +57,7 @@ struct ErrorMeta {
2457
context: &'static str,
2558
location: &'static core::panic::Location<'static>,
2659
source: Option<Box<dyn Source>>,
60+
position: Option<ErrorPosition>,
2761
}
2862

2963
/// A typed error wrapper carrying a `Kind` discriminant plus diagnostic metadata.
@@ -53,9 +87,14 @@ impl<Kind: fmt::Debug> fmt::Debug for Error<Kind> {
5387
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5488
let mut dbg = f.debug_struct("Error");
5589
#[cfg(feature = "alloc")]
56-
dbg.field("context", &self.meta.context)
57-
.field("kind", &self.kind)
58-
.field("source", &self.meta.source);
90+
{
91+
dbg.field("context", &self.meta.context)
92+
.field("kind", &self.kind)
93+
.field("source", &self.meta.source);
94+
if let Some(position) = &self.meta.position {
95+
dbg.field("position", position);
96+
}
97+
}
5998
#[cfg(not(feature = "alloc"))]
6099
dbg.field("context", &self.context).field("kind", &self.kind);
61100
dbg.finish()
@@ -74,6 +113,7 @@ impl<Kind> Error<Kind> {
74113
context,
75114
location: core::panic::Location::caller(),
76115
source: None,
116+
position: None,
77117
}),
78118
#[cfg(not(feature = "alloc"))]
79119
context,
@@ -130,6 +170,60 @@ impl<Kind> Error<Kind> {
130170
self.meta.location
131171
}
132172

173+
/// Records the byte offset in the input stream where this error was detected.
174+
///
175+
/// Composes with [`Error::in_field`]: both may be set independently and
176+
/// are rendered together in `Display` output when present.
177+
///
178+
/// Primarily intended for decode and encode errors on wire-format input,
179+
/// where pointing at the offending byte materially aids triage and is
180+
/// strictly more informative than the source code location alone.
181+
///
182+
/// Available only when the `alloc` feature is enabled.
183+
#[cfg(feature = "alloc")]
184+
#[cold]
185+
#[must_use]
186+
pub fn at_offset(mut self, offset: usize) -> Self {
187+
let field = self.meta.position.and_then(|p| p.field);
188+
self.meta.position = Some(ErrorPosition {
189+
offset: Some(offset),
190+
field,
191+
});
192+
self
193+
}
194+
195+
/// Records the symbolic field or sub-PDU name being processed when this
196+
/// error occurred.
197+
///
198+
/// Composes with [`Error::at_offset`]: both may be set independently and
199+
/// are rendered together in `Display` output when present.
200+
///
201+
/// Dotted notation may be used for nested PDU navigation,
202+
/// e.g. `"ServerSecurityData.serverRandom"`.
203+
///
204+
/// Available only when the `alloc` feature is enabled.
205+
#[cfg(feature = "alloc")]
206+
#[cold]
207+
#[must_use]
208+
pub fn in_field(mut self, field: &'static str) -> Self {
209+
let offset = self.meta.position.and_then(|p| p.offset);
210+
self.meta.position = Some(ErrorPosition {
211+
offset,
212+
field: Some(field),
213+
});
214+
self
215+
}
216+
217+
/// Returns the byte offset and/or field-context position at which this
218+
/// error was constructed, if either was recorded via [`Error::at_offset`]
219+
/// or [`Error::in_field`].
220+
///
221+
/// Available only when the `alloc` feature is enabled.
222+
#[cfg(feature = "alloc")]
223+
pub fn position(&self) -> Option<&ErrorPosition> {
224+
self.meta.position.as_ref()
225+
}
226+
133227
pub fn set_context(&mut self, context: &'static str) {
134228
#[cfg(feature = "alloc")]
135229
{
@@ -160,7 +254,26 @@ where
160254
self.meta.location.file(),
161255
self.meta.location.line(),
162256
self.kind
163-
)
257+
)?;
258+
match self.meta.position {
259+
Some(ErrorPosition {
260+
offset: Some(o),
261+
field: Some(name),
262+
}) => write!(f, " (at offset {o} in {name:?})"),
263+
Some(ErrorPosition {
264+
offset: Some(o),
265+
field: None,
266+
}) => write!(f, " (at offset {o})"),
267+
Some(ErrorPosition {
268+
offset: None,
269+
field: Some(name),
270+
}) => write!(f, " (in {name:?})"),
271+
Some(ErrorPosition {
272+
offset: None,
273+
field: None,
274+
})
275+
| None => Ok(()),
276+
}
164277
}
165278
#[cfg(not(feature = "alloc"))]
166279
{

0 commit comments

Comments
 (0)