Skip to main content

oopsie_core/
lib.rs

1//! Core error types and utilities.
2//!
3//! `std` is on by default; build with `default-features = false` for `no_std` +
4//! `alloc` targets (an allocator is always required — bring your own with
5//! `extern crate alloc` and a global allocator in the consumer). Backtrace
6//! capture, panic-hook integration, `tracing`, and clock-based timestamps
7//! (`chrono` / `jiff`) require `std`; under `no_std`, [`Backtrace`] is an
8//! always-empty stub so the rest of the API keeps its shape.
9
10#![warn(missing_docs)]
11#![cfg_attr(
12    feature = "unstable-error-generic-member-access",
13    feature(error_generic_member_access)
14)]
15// Doctests that use `#[derive(Oopsie)]` will see the macro emit a `provide`
16// impl when the unstable feature is active; inject the corresponding language
17// feature flag so those doctests compile under `--features unstable`.
18#![cfg_attr(
19    feature = "unstable-error-generic-member-access",
20    doc(test(attr(feature(error_generic_member_access))))
21)]
22#![cfg_attr(not(feature = "std"), no_std)]
23
24pub extern crate alloc;
25
26#[cfg(feature = "std")]
27mod backtrace;
28mod chain;
29mod diagnostic;
30#[cfg(feature = "serde")]
31pub mod erased;
32#[cfg(feature = "extras")]
33pub mod extras;
34#[cfg(feature = "std")]
35mod marker;
36#[cfg(not(feature = "std"))]
37mod nostd_stubs;
38mod spantrace;
39#[cfg(feature = "test-utils")]
40pub mod test_utils;
41mod traits;
42mod welp;
43
44use alloc::borrow::Cow;
45use alloc::string::String;
46use core::borrow::Borrow;
47use core::ops::Deref;
48
49#[cfg(feature = "std")]
50pub use backtrace::{
51    Backtrace, RustBacktrace, clear_rust_backtrace_override, rust_backtrace, rust_panic_backtrace,
52    set_rust_backtrace_override, with_rust_backtrace_override,
53};
54pub use chain::{Chain, ErrorChainExt};
55pub use diagnostic::Diagnostic;
56#[cfg(not(feature = "std"))]
57pub use nostd_stubs::Backtrace;
58pub use spantrace::{OptionalSpanTrace, SpanTrace, SpanTraceStatus};
59#[cfg(feature = "tracing")]
60pub mod tracing;
61
62pub use traits::*;
63pub use welp::{Welp, WelpOptionExt, WelpResultExt};
64/// Private helpers used by macro-generated code. Not part of the public API.
65#[doc(hidden)]
66pub mod __private {
67    #[cfg(feature = "std")]
68    pub use crate::backtrace::CORE_SRC_PATH;
69    /// Autoref probe for capture deduplication.
70    ///
71    /// When the concrete source type implements `Diagnostic`, the high-priority
72    /// `CaptureFromExt` impl is selected and tries to extract existing traces.
73    /// For non-`Diagnostic` sources (e.g., `io::Error`), the low-priority
74    /// `CaptureFromFallback` impl is selected via autoref and does fresh capture.
75    pub struct CaptureProbe<'a, T: ?Sized>(pub &'a T);
76
77    /// High-priority: source implements `Diagnostic` → try extraction.
78    pub trait CaptureFromExt {
79        fn resolve<C: crate::Capturable>(&self) -> C;
80    }
81
82    impl<T: crate::Diagnostic> CaptureFromExt for CaptureProbe<'_, T> {
83        #[inline]
84        #[track_caller]
85        fn resolve<C: crate::Capturable>(&self) -> C {
86            C::capture_or_extract(self.0)
87        }
88    }
89
90    /// Low-priority: source doesn't implement `Diagnostic` → fresh capture.
91    pub trait CaptureFromFallback {
92        fn resolve<C: crate::Capturable>(&self) -> C;
93    }
94
95    impl<T: ?Sized> CaptureFromFallback for &CaptureProbe<'_, T> {
96        #[inline]
97        #[track_caller]
98        fn resolve<C: crate::Capturable>(&self) -> C {
99            C::capture()
100        }
101    }
102
103    /// Accessor-time autoref probe for forwarding a transparent wrapper's
104    /// `Diagnostic` accessors to its source.
105    ///
106    /// Unlike [`CaptureProbe`] (which runs at construction to decide
107    /// extract-vs-capture), this runs when `oopsie_*` is called: a transparent
108    /// variant delegates each accessor to its source's concrete type. When that
109    /// type implements `Diagnostic` the high-priority [`DiagForwardExt`] impl
110    /// forwards the call; otherwise the [`DiagForwardFallback`] impl (selected
111    /// via autoref) yields `None`. Works on stable, where the Provider-API
112    /// [`source_trace`] path would yield `None`.
113    pub struct DiagProbe<'a, T: ?Sized>(pub &'a T);
114
115    /// High-priority: source implements `Diagnostic` → forward the accessor.
116    pub trait DiagForwardExt<'a> {
117        fn fwd_code(&self) -> Option<crate::ErrorCode>;
118        fn fwd_help(&self) -> Option<crate::HelpText>;
119        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace>;
120        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace>;
121        fn fwd_location(&self) -> Option<&'static core::panic::Location<'static>>;
122        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8>;
123    }
124
125    impl<'a, T: crate::Diagnostic + ?Sized> DiagForwardExt<'a> for DiagProbe<'a, T> {
126        #[inline]
127        fn fwd_code(&self) -> Option<crate::ErrorCode> {
128            self.0.oopsie_error_code()
129        }
130        #[inline]
131        fn fwd_help(&self) -> Option<crate::HelpText> {
132            self.0.oopsie_help_text()
133        }
134        #[inline]
135        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace> {
136            self.0.oopsie_backtrace()
137        }
138        #[inline]
139        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace> {
140            self.0.oopsie_spantrace()
141        }
142        #[inline]
143        fn fwd_location(&self) -> Option<&'static core::panic::Location<'static>> {
144            self.0.oopsie_location()
145        }
146        #[inline]
147        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8> {
148            self.0.oopsie_exit_code()
149        }
150    }
151
152    /// Low-priority: source doesn't implement `Diagnostic` → nothing to forward.
153    pub trait DiagForwardFallback<'a> {
154        #[inline]
155        fn fwd_code(&self) -> Option<crate::ErrorCode> {
156            None
157        }
158        #[inline]
159        fn fwd_help(&self) -> Option<crate::HelpText> {
160            None
161        }
162        #[inline]
163        fn fwd_backtrace(&self) -> Option<&'a crate::Backtrace> {
164            None
165        }
166        #[inline]
167        fn fwd_spantrace(&self) -> Option<&'a crate::SpanTrace> {
168            None
169        }
170        #[inline]
171        fn fwd_location(&self) -> Option<&'static core::panic::Location<'static>> {
172            None
173        }
174        #[inline]
175        fn fwd_exit_code(&self) -> Option<core::num::NonZeroU8> {
176            None
177        }
178    }
179
180    impl<'a, T: ?Sized> DiagForwardFallback<'a> for &DiagProbe<'a, T> {}
181
182    /// Pull the deepest `T` reachable from a source error via the Provider API.
183    ///
184    /// The generated `provide()` forwards to the source before providing its
185    /// own trace, and std's `Request` is first-wins, so the deepest provider in
186    /// the chain fills the slot — this surfaces the origin-most trace rather
187    /// than a wrap-site one. Trace accessors go through the typed
188    /// [`source_backtrace`] / `source_spantrace` wrappers, which add the
189    /// skip-empty filter on top of this lookup.
190    ///
191    /// Returns `None` without `unstable-error-generic-member-access`: descending
192    /// into a type-erased `dyn Error` source is not portable there, and the
193    /// caller falls back to the wrapper's own field.
194    #[inline]
195    #[must_use]
196    pub fn source_trace<'a, T: 'static>(
197        source: &'a (dyn core::error::Error + 'static),
198    ) -> Option<&'a T> {
199        #[cfg(feature = "unstable-error-generic-member-access")]
200        {
201            core::error::request_ref::<T>(source)
202        }
203        #[cfg(not(feature = "unstable-error-generic-member-access"))]
204        {
205            let _ = source;
206            None
207        }
208    }
209
210    /// [`source_trace`] for `Backtrace`, treating an empty trace as absent:
211    /// a trace that captured no frames is not "available" and must not shadow
212    /// a captured one further out.
213    #[inline]
214    #[must_use]
215    pub fn source_backtrace<'a>(
216        source: &'a (dyn core::error::Error + 'static),
217    ) -> Option<&'a crate::Backtrace> {
218        source_trace::<crate::Backtrace>(source).filter(|bt| bt.is_captured())
219    }
220
221    /// [`source_trace`] for `SpanTrace`, treating an empty trace as absent.
222    #[inline]
223    #[must_use]
224    pub fn source_spantrace<'a>(
225        source: &'a (dyn core::error::Error + 'static),
226    ) -> Option<&'a crate::SpanTrace> {
227        source_trace::<crate::SpanTrace>(source).filter(|st| st.is_captured())
228    }
229
230    #[cfg(feature = "std")]
231    pub use crate::marker::{TraceMarker, restore_marker, set_marker};
232    #[cfg(not(feature = "std"))]
233    pub use crate::nostd_stubs::{TraceMarker, restore_marker, set_marker};
234
235    /// The exit code a type-erased source declares, reached through the Provider
236    /// API. Returns `None` on stable (descending into a `dyn Error` is not
237    /// portable there).
238    #[inline]
239    #[must_use]
240    pub fn source_exit_code(
241        source: &(dyn core::error::Error + 'static),
242    ) -> Option<core::num::NonZeroU8> {
243        #[cfg(feature = "unstable-error-generic-member-access")]
244        {
245            core::error::request_value::<core::num::NonZeroU8>(source)
246        }
247        #[cfg(not(feature = "unstable-error-generic-member-access"))]
248        {
249            let _ = source;
250            None
251        }
252    }
253
254    /// Build a `NonZeroU8` from a value the macro already validated to be in
255    /// `1..=255`. Panics at const-eval if the invariant is ever broken, so the
256    /// generated code stays `unsafe`-free.
257    #[inline]
258    #[must_use]
259    pub const fn nonzero_u8_unchecked(value: u8) -> core::num::NonZeroU8 {
260        match core::num::NonZeroU8::new(value) {
261            Some(n) => n,
262            None => panic!("exit code must be non-zero"),
263        }
264    }
265
266    /// Fixed-capacity const string builder: concatenate string slices and base-10
267    /// `usize`s in a `const` context, then borrow the result as `&str` — e.g. to
268    /// feed a const `panic!("{}", …)` with a computed message. `N` is the byte
269    /// capacity; callers size it from the pieces (`Σ part.len() + 20·#usize`) so a
270    /// push never runs past the buffer.
271    pub struct ConstStr<const N: usize> {
272        buf: [u8; N],
273        len: usize,
274    }
275
276    impl<const N: usize> ConstStr<N> {
277        #[inline]
278        #[must_use]
279        pub const fn new() -> Self {
280            Self {
281                buf: [0; N],
282                len: 0,
283            }
284        }
285
286        /// Append a string slice (its UTF-8 bytes verbatim).
287        #[inline]
288        #[must_use]
289        pub const fn str(mut self, s: &str) -> Self {
290            let b = s.as_bytes();
291            let mut i = 0;
292            while i < b.len() {
293                self.buf[self.len] = b[i];
294                self.len += 1;
295                i += 1;
296            }
297            self
298        }
299
300        /// Append a `usize` in base 10.
301        #[inline]
302        #[must_use]
303        pub const fn usize(mut self, mut n: usize) -> Self {
304            let mut digits = [0u8; 20];
305            let mut count = if n == 0 {
306                digits[0] = b'0';
307                1
308            } else {
309                0
310            };
311            while n > 0 {
312                digits[count] = b'0' + (n % 10) as u8;
313                n /= 10;
314                count += 1;
315            }
316            while count > 0 {
317                count -= 1;
318                self.buf[self.len] = digits[count];
319                self.len += 1;
320            }
321            self
322        }
323
324        /// Borrow the accumulated bytes as `&str`. Only valid UTF-8 and ASCII digits
325        /// are ever written, so the error branch is unreachable in correct use.
326        #[inline]
327        #[must_use]
328        pub const fn as_str(&self) -> &str {
329            match core::str::from_utf8(self.buf.split_at(self.len).0) {
330                Ok(s) => s,
331                Err(_) => panic!("ConstStr: built a non-utf8 message"),
332            }
333        }
334    }
335
336    impl<const N: usize> Default for ConstStr<N> {
337        fn default() -> Self {
338            Self::new()
339        }
340    }
341
342    #[cfg(feature = "chrono")]
343    pub use chrono;
344
345    /// `SystemTime` facade for macro-generated timestamp capture: routes
346    /// through here so generated code names one path regardless of std
347    /// availability, rather than `::std::time::SystemTime` directly.
348    #[cfg(feature = "std")]
349    pub use std::time::SystemTime;
350
351    /// `alloc` facade for macro-generated code: a bare `::alloc::` path only
352    /// resolves in a crate that declared `extern crate alloc;` itself, which
353    /// std-linked consumers of `#[oopsie]`/`#[derive(Oopsie)]` never do.
354    /// Routing through here instead names one path that resolves in both std
355    /// and no_std consumers, since this crate always declares `extern crate
356    /// alloc;`.
357    pub use alloc;
358}
359
360macro_rules! impl_string_newtypes {
361    ($($(#[$meta:meta])* $ident:ident,)* $(,)?) => { $(
362        #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
363        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
364        #[cfg_attr(feature = "serde", serde(transparent))]
365        #[repr(transparent)]
366        $(#[$meta])*
367        pub struct $ident(Cow<'static, str>);
368
369        impl $ident {
370            #[doc = concat!("Build a new `", stringify!($ident), "` from a `&'static str`.")]
371            #[must_use]
372            #[inline]
373            pub const fn from_static(s: &'static str) -> Self {
374                Self(Cow::Borrowed(s))
375            }
376
377            #[doc = concat!("Build a new `", stringify!($ident), "` from a `String`.")]
378            #[must_use]
379            #[inline]
380            pub const fn from_string(s: String) -> Self {
381                Self(Cow::Owned(s))
382            }
383
384            /// Borrow the underlying string.
385            #[must_use]
386            #[inline]
387            pub const fn as_str(&self) -> &str {
388                match &self.0 {
389                    Cow::Borrowed(s) => s,
390                    Cow::Owned(s) => s.as_str(),
391                }
392            }
393
394            #[doc = concat!("Consume the `", stringify!($ident), "` and return the underlying `Cow`.")]
395            #[must_use]
396            #[inline]
397            pub fn into_inner(self) -> Cow<'static, str> {
398                self.0
399            }
400        }
401
402        impl Deref for $ident {
403            type Target = str;
404
405            #[inline]
406            fn deref(&self) -> &Self::Target {
407                self.as_str()
408            }
409        }
410
411        impl Borrow<str> for $ident {
412            #[inline]
413            fn borrow(&self) -> &str {
414                self.as_str()
415            }
416        }
417
418        impl From<&'static str> for $ident {
419            #[inline]
420            fn from(s: &'static str) -> Self {
421                Self::from_static(s)
422            }
423        }
424
425        impl From<String> for $ident {
426            #[inline]
427            fn from(s: String) -> Self {
428                Self::from_string(s)
429            }
430        }
431
432        impl core::fmt::Display for $ident {
433            #[inline]
434            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
435                self.0.fmt(f)
436            }
437        }
438
439    )* };
440}
441
442impl_string_newtypes!(
443    /// An opaque error code, used for programmatic handling and matching.
444    ErrorCode,
445    /// User-facing help text, intended to be shown in diagnostics.
446    HelpText,
447);
448
449#[cfg(test)]
450mod const_str_tests {
451    use crate::__private::ConstStr;
452
453    #[test]
454    fn builds_interleaved_string() {
455        let s = ConstStr::<64>::new()
456            .str("`E` is ")
457            .usize(80)
458            .str(" bytes, must be ≤ 64");
459        assert_eq!(s.as_str(), "`E` is 80 bytes, must be ≤ 64");
460    }
461
462    #[test]
463    fn formats_zero_and_large() {
464        assert_eq!(ConstStr::<8>::new().as_str(), "");
465        assert_eq!(ConstStr::<32>::new().usize(0).as_str(), "0");
466        assert_eq!(
467            ConstStr::<32>::new().usize(18446744073709551615).as_str(),
468            "18446744073709551615"
469        );
470    }
471
472    #[test]
473    fn usable_in_const_context() {
474        const M: &str = {
475            const C: ConstStr<16> = ConstStr::new().str("n=").usize(42);
476            C.as_str()
477        };
478        assert_eq!(M, "n=42");
479    }
480}