Skip to content

Commit 4ff9ed5

Browse files
Greg Lambersonlamco-office
authored andcommitted
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 Devolutions#1257, Devolutions#1120. Depends on Devolutions#1269.
1 parent 2e2699d commit 4ff9ed5

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.
@@ -56,9 +90,14 @@ impl<Kind: fmt::Debug> fmt::Debug for Error<Kind> {
5690
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5791
let mut dbg = f.debug_struct("Error");
5892
#[cfg(feature = "alloc")]
59-
dbg.field("context", &self.meta.context)
60-
.field("kind", &self.kind)
61-
.field("source", &self.meta.source);
93+
{
94+
dbg.field("context", &self.meta.context)
95+
.field("kind", &self.kind)
96+
.field("source", &self.meta.source);
97+
if let Some(position) = &self.meta.position {
98+
dbg.field("position", position);
99+
}
100+
}
62101
#[cfg(not(feature = "alloc"))]
63102
dbg.field("context", &self.context).field("kind", &self.kind);
64103
dbg.finish()
@@ -77,6 +116,7 @@ impl<Kind> Error<Kind> {
77116
context,
78117
location: core::panic::Location::caller(),
79118
source: None,
119+
position: None,
80120
}),
81121
#[cfg(not(feature = "alloc"))]
82122
context,
@@ -141,6 +181,60 @@ impl<Kind> Error<Kind> {
141181
}
142182
}
143183

184+
/// Records the byte offset in the input stream where this error was detected.
185+
///
186+
/// Composes with [`Error::in_field`]: both may be set independently and
187+
/// are rendered together in `Display` output when present.
188+
///
189+
/// Primarily intended for decode and encode errors on wire-format input,
190+
/// where pointing at the offending byte materially aids triage and is
191+
/// strictly more informative than the source code location alone.
192+
///
193+
/// Available only when the `alloc` feature is enabled.
194+
#[cfg(feature = "alloc")]
195+
#[cold]
196+
#[must_use]
197+
pub fn at_offset(mut self, offset: usize) -> Self {
198+
let field = self.meta.position.and_then(|p| p.field);
199+
self.meta.position = Some(ErrorPosition {
200+
offset: Some(offset),
201+
field,
202+
});
203+
self
204+
}
205+
206+
/// Records the symbolic field or sub-PDU name being processed when this
207+
/// error occurred.
208+
///
209+
/// Composes with [`Error::at_offset`]: both may be set independently and
210+
/// are rendered together in `Display` output when present.
211+
///
212+
/// Dotted notation may be used for nested PDU navigation,
213+
/// e.g. `"ServerSecurityData.serverRandom"`.
214+
///
215+
/// Available only when the `alloc` feature is enabled.
216+
#[cfg(feature = "alloc")]
217+
#[cold]
218+
#[must_use]
219+
pub fn in_field(mut self, field: &'static str) -> Self {
220+
let offset = self.meta.position.and_then(|p| p.offset);
221+
self.meta.position = Some(ErrorPosition {
222+
offset,
223+
field: Some(field),
224+
});
225+
self
226+
}
227+
228+
/// Returns the byte offset and/or field-context position at which this
229+
/// error was constructed, if either was recorded via [`Error::at_offset`]
230+
/// or [`Error::in_field`].
231+
///
232+
/// Available only when the `alloc` feature is enabled.
233+
#[cfg(feature = "alloc")]
234+
pub fn position(&self) -> Option<&ErrorPosition> {
235+
self.meta.position.as_ref()
236+
}
237+
144238
pub fn set_context(&mut self, context: &'static str) {
145239
#[cfg(feature = "alloc")]
146240
{
@@ -171,7 +265,26 @@ where
171265
self.meta.location.file(),
172266
self.meta.location.line(),
173267
self.kind
174-
)
268+
)?;
269+
match self.meta.position {
270+
Some(ErrorPosition {
271+
offset: Some(o),
272+
field: Some(name),
273+
}) => write!(f, " (at offset {o} in {name:?})"),
274+
Some(ErrorPosition {
275+
offset: Some(o),
276+
field: None,
277+
}) => write!(f, " (at offset {o})"),
278+
Some(ErrorPosition {
279+
offset: None,
280+
field: Some(name),
281+
}) => write!(f, " (in {name:?})"),
282+
Some(ErrorPosition {
283+
offset: None,
284+
field: None,
285+
})
286+
| None => Ok(()),
287+
}
175288
}
176289
#[cfg(not(feature = "alloc"))]
177290
{

0 commit comments

Comments
 (0)