Skip to main content

ironrdp_error/
lib.rs

1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
3#![cfg_attr(not(feature = "std"), no_std)]
4
5#[cfg(feature = "alloc")]
6extern crate alloc;
7
8#[cfg(all(feature = "alloc", not(feature = "std")))]
9use alloc::boxed::Box;
10use core::fmt;
11
12pub trait Source: core::error::Error + Send + Sync + 'static {}
13
14impl<T> Source for T where T: core::error::Error + Send + Sync + 'static {}
15
16/// Diagnostic metadata stored behind a [`Box`] so that `Error<Kind>` stays small.
17///
18/// All fields here are purely for display and error-chain traversal; none are
19/// needed for matching on the error kind. The allocation only occurs when an
20/// error is *constructed* — a cold path — so the per-error heap cost is
21/// acceptable.
22#[cfg(feature = "alloc")]
23struct ErrorMeta {
24    context: &'static str,
25    location: &'static core::panic::Location<'static>,
26    source: Option<Box<dyn Source>>,
27}
28
29/// A typed error wrapper carrying a `Kind` discriminant plus diagnostic metadata.
30///
31/// # `no_alloc` platforms
32///
33/// When compiled without the `alloc` feature, `Error<Kind>` retains `kind`,
34/// `context`, and `location` inline. The error source chain is unavailable.
35/// `no_alloc` targets are supported on a best-effort basis and are not a
36/// primary target of this crate. Do not add more inline fields here: the
37/// struct should stay lean for stack-constrained environments.
38pub struct Error<Kind> {
39    kind: Kind,
40    /// Diagnostic metadata. Present only when `alloc` is available.
41    #[cfg(feature = "alloc")]
42    meta: Box<ErrorMeta>,
43    /// Minimal context kept for `no_alloc` targets (no source chain).
44    #[cfg(not(feature = "alloc"))]
45    context: &'static str,
46    #[cfg(not(feature = "alloc"))]
47    location: &'static core::panic::Location<'static>,
48}
49
50// Manual `Debug` impl that excludes the `location` field. The location is
51// captured via `core::panic::Location::caller()` and rendered in `Display`,
52// but its `file()` returns platform-native paths (`/` on Unix, `\` on
53// Windows). Including it in `Debug` would break cross-platform snapshot
54// tests. Consumers needing programmatic access can use `Error::location()`.
55impl<Kind: fmt::Debug> fmt::Debug for Error<Kind> {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        let mut dbg = f.debug_struct("Error");
58        #[cfg(feature = "alloc")]
59        dbg.field("context", &self.meta.context)
60            .field("kind", &self.kind)
61            .field("source", &self.meta.source);
62        #[cfg(not(feature = "alloc"))]
63        dbg.field("context", &self.context).field("kind", &self.kind);
64        dbg.finish()
65    }
66}
67
68impl<Kind> Error<Kind> {
69    #[cold]
70    #[must_use]
71    #[track_caller]
72    pub fn new(context: &'static str, kind: Kind) -> Self {
73        Self {
74            kind,
75            #[cfg(feature = "alloc")]
76            meta: Box::new(ErrorMeta {
77                context,
78                location: core::panic::Location::caller(),
79                source: None,
80            }),
81            #[cfg(not(feature = "alloc"))]
82            context,
83            #[cfg(not(feature = "alloc"))]
84            location: core::panic::Location::caller(),
85        }
86    }
87
88    #[cold]
89    #[must_use]
90    pub fn with_source<E>(self, source: E) -> Self
91    where
92        E: Source,
93    {
94        #[cfg(feature = "alloc")]
95        {
96            let mut this = self;
97            this.meta.source = Some(Box::new(source));
98            this
99        }
100
101        // No source when no alloc
102        #[cfg(not(feature = "alloc"))]
103        {
104            let _ = source;
105            self
106        }
107    }
108
109    pub fn kind(&self) -> &Kind {
110        &self.kind
111    }
112
113    /// Returns the source code location at which this error was constructed.
114    ///
115    /// Captured automatically by [`Error::new`] via [`core::panic::Location::caller`]
116    /// and `#[track_caller]`. Useful for diagnostic logging and error reporting
117    /// when the variant alone does not narrow down the call site enough.
118    pub fn location(&self) -> &'static core::panic::Location<'static> {
119        #[cfg(feature = "alloc")]
120        {
121            self.meta.location
122        }
123        #[cfg(not(feature = "alloc"))]
124        {
125            self.location
126        }
127    }
128
129    pub fn set_context(&mut self, context: &'static str) {
130        #[cfg(feature = "alloc")]
131        {
132            self.meta.context = context;
133        }
134        #[cfg(not(feature = "alloc"))]
135        {
136            self.context = context;
137        }
138    }
139
140    pub fn report(&self) -> ErrorReport<'_, Kind> {
141        ErrorReport(self)
142    }
143}
144
145impl<Kind> fmt::Display for Error<Kind>
146where
147    Kind: fmt::Display,
148{
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        #[cfg(feature = "alloc")]
151        {
152            write!(
153                f,
154                "[{} @ {}:{}] {}",
155                self.meta.context,
156                self.meta.location.file(),
157                self.meta.location.line(),
158                self.kind
159            )
160        }
161        #[cfg(not(feature = "alloc"))]
162        {
163            write!(
164                f,
165                "[{} @ {}:{}] {}",
166                self.context,
167                self.location.file(),
168                self.location.line(),
169                self.kind
170            )
171        }
172    }
173}
174
175#[cfg(feature = "std")]
176impl<Kind> core::error::Error for Error<Kind>
177where
178    Kind: core::error::Error,
179{
180    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
181        if let Some(source) = self.kind.source() {
182            Some(source)
183        } else {
184            // NOTE: we can't use Option::as_ref here because of type inference
185            if let Some(e) = &self.meta.source {
186                Some(e.as_ref())
187            } else {
188                None
189            }
190        }
191    }
192}
193
194#[cfg(feature = "std")]
195impl<Kind> From<Error<Kind>> for std::io::Error
196where
197    Kind: core::error::Error + Send + Sync + 'static,
198{
199    fn from(error: Error<Kind>) -> Self {
200        Self::other(error)
201    }
202}
203
204pub struct ErrorReport<'a, Kind>(&'a Error<Kind>);
205
206#[cfg(feature = "std")]
207impl<Kind> fmt::Display for ErrorReport<'_, Kind>
208where
209    Kind: core::error::Error,
210{
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        use core::error::Error as _;
213
214        write!(f, "{}", self.0)?;
215
216        let mut next_source = self.0.source();
217
218        while let Some(e) = next_source {
219            write!(f, ", caused by: {e}")?;
220            next_source = e.source();
221        }
222
223        Ok(())
224    }
225}
226
227#[cfg(not(feature = "std"))]
228impl<E> fmt::Display for ErrorReport<'_, E>
229where
230    E: fmt::Display,
231{
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        write!(f, "{}", self.0)?;
234
235        #[cfg(feature = "alloc")]
236        if let Some(source) = &self.0.meta.source {
237            write!(f, ", caused by: {source}")?;
238        }
239
240        Ok(())
241    }
242}
243
244/// Returns from the enclosing function with an [`Error`] built from a kind variant.
245///
246/// Three forms are supported:
247///
248/// - `bail!(kind)` — empty context.
249/// - `bail!(context, kind)` — explicit `&'static str` context.
250/// - `bail!(context, kind, source: source)` — explicit context plus a chained source error.
251///
252/// The kind type is inferred from the enclosing function's return type, which must be
253/// `Result<_, Error<Kind>>` (or any type alias resolving to it).
254///
255/// Mirrors the call-site shape of [`anyhow::bail!`] but produces a typed
256/// `Error<Kind>` rather than a type-erased `anyhow::Error`.
257///
258/// [`anyhow::bail!`]: https://docs.rs/anyhow/latest/anyhow/macro.bail.html
259#[macro_export]
260macro_rules! bail {
261    ($kind:expr $(,)?) => {
262        return ::core::result::Result::Err($crate::Error::new("", $kind))
263    };
264    ($context:expr, $kind:expr $(,)?) => {
265        return ::core::result::Result::Err($crate::Error::new($context, $kind))
266    };
267    ($context:expr, $kind:expr, source: $source:expr $(,)?) => {
268        return ::core::result::Result::Err($crate::Error::new($context, $kind).with_source($source))
269    };
270}
271
272/// Returns from the enclosing function with an [`Error`] if the given condition is false.
273///
274/// Two forms are supported:
275///
276/// - `ensure!(condition, kind)` — empty context.
277/// - `ensure!(condition, context, kind)` — explicit `&'static str` context.
278///
279/// The kind type is inferred from the enclosing function's return type, which must be
280/// `Result<_, Error<Kind>>` (or any type alias resolving to it).
281///
282/// Mirrors the call-site shape of [`anyhow::ensure!`] but produces a typed
283/// `Error<Kind>` rather than a type-erased `anyhow::Error`.
284///
285/// [`anyhow::ensure!`]: https://docs.rs/anyhow/latest/anyhow/macro.ensure.html
286#[macro_export]
287macro_rules! ensure {
288    ($condition:expr, $kind:expr $(,)?) => {
289        if !($condition) {
290            return ::core::result::Result::Err($crate::Error::new("", $kind));
291        }
292    };
293    ($condition:expr, $context:expr, $kind:expr $(,)?) => {
294        if !($condition) {
295            return ::core::result::Result::Err($crate::Error::new($context, $kind));
296        }
297    };
298}