Skip to main content

thruput_sdk/exn/
raw.rs

1//! The two-tier exception design:
2//!
3//! - [`RawExceptionRef`] is the transient, borrowed form. It only exists
4//!   inside the `on_exn` dispatch closure of
5//!   [`try_call_raw_with`](crate::call::try_call_raw_with) — its payload is
6//!   a plain slice borrowed straight from the call scratch, valid exactly
7//!   for the closure's lifetime, so it can never be observed stale.
8//! - [`RawException`] is the owned form, produced by
9//!   [`RawExceptionRef::to_owned`] when a caller needs to hold onto an
10//!   exception past the call (one copy).
11//!
12//! Under `alloc` the copy is an owned exact-size heap buffer. In the
13//! no-alloc guest the copy is an inline fixed [`SCRATCH_MAX`]-capacity
14//! buffer embedded in the value (the same buffer type backing the call
15//! scratch), so `RawException` — and any error enum containing one — is
16//! roughly 4 KiB. That size is a deliberate trade of stack for uniformity:
17//! catch semantics are identical in both builds — each `RawException` owns
18//! its buffer, so any number may be live concurrently, the same as under
19//! `alloc`. The cost is paid per live value (stack, not a static), and the
20//! memcpy that fills it only runs when an undecodable exception is actually
21//! caught.
22//!
23//! Borrowing avoids an additional owned exception buffer in guest memory.
24//! The host still copies returned or abort bytes into guest scratch, and
25//! rethrowing copies the wire bytes back to the host. Decoding an exception
26//! with owned fields can also copy or allocate.
27//!
28//! [`SCRATCH_MAX`]: crate::internal::SCRATCH_MAX
29
30use crate::exn::Exn;
31#[cfg(feature = "alloc")]
32use crate::exn::{CallingError, FaultString, LibraryExn};
33use crate::xdr::*;
34use thruput_exn::RawExceptionRef;
35
36/// Re-throw to our caller: abort with the original wire bytes, verbatim.
37/// No additional guest buffer; the return syscall still copies bytes to the
38/// host. This is what plain (non-`try_`) generated methods do.
39impl crate::re_throw::ReThrow for RawExceptionRef<'_> {
40    #[inline]
41    #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
42    fn re_throw(self) -> ! {
43        #[cfg(all(not(target_arch = "riscv32"), feature = "std"))]
44        crate::contract_op::abort_breadcrumb("Re-throwing", "Location", &self);
45
46        crate::contract_op::contract_abort(self.wire())
47    }
48}
49
50/// What [`RawExceptionRef::to_owned`] produces: one copy. Any number may
51/// be live at once; without `alloc` each value carries its own fixed
52/// SCRATCH_MAX-capacity buffer.
53impl From<RawExceptionRef<'_>> for RawException {
54    fn from(exn: RawExceptionRef<'_>) -> Self {
55        RawException::own_wire(&exn)
56    }
57}
58
59/// `Debug` body for [`RawException`], mirroring [`RawExceptionRef`]'s.
60fn fmt_exn(
61    f: &mut core::fmt::Formatter,
62    name: &str,
63    selector: u64,
64    payload: &[u8],
65) -> core::fmt::Result {
66    write!(
67        f,
68        "{name} {{ selector: 0x{selector:016X}, payload: {} bytes",
69        payload.len(),
70    )?;
71    #[cfg(not(target_arch = "riscv32"))]
72    write!(f, ", hex: {}", crate::hex::hex_encode(&payload))?;
73    write!(f, " }}")
74}
75
76/// An exception caught and detached from the call scratch: the payload is
77/// copied once, into storage owned by the value itself, so it survives
78/// arbitrary later SDK operations (including re-use of the call scratch).
79/// Without `alloc` that storage is a fixed `SCRATCH_MAX`-capacity inline
80/// buffer, so the value (and any error enum holding it) is roughly 4 KiB;
81/// under `alloc` it is an exact-size heap allocation.
82/// Use [`MapExceptions`](crate::MapExceptions) when a compact classification
83/// is enough and the original wire bytes do not need to outlive the call.
84///
85/// Deliberately not encodable: there is no `WriteXDR` impl.
86/// [`re_throw`](Self::re_throw) aborts with the verbatim wire bytes;
87/// [`try_as`](Self::try_as) decodes to a typed exception, with
88/// [`selector`](Self::selector) / [`payload`](Self::payload) for raw
89/// inspection.
90pub struct RawException {
91    selector: u64,
92    /// Full abort wire bytes: `[CXS(selector)][payload]` — kept verbatim so
93    /// `re_throw` preserves multi-hop fidelity.
94    wire: crate::call::Buf,
95    payload_start: usize,
96}
97
98impl RawException {
99    /// Copy `exn`'s wire bytes out of the caller's borrow into owned
100    /// storage, carrying over its already-parsed `selector`/`payload_start`
101    /// so the two stay consistent by construction.
102    ///
103    /// Called by the `From<RawExceptionRef>` impl.
104    fn own_wire(exn: &RawExceptionRef<'_>) -> Self {
105        let selector = exn.selector();
106        let payload_start = exn.payload_start();
107        let wire = exn.wire();
108        #[cfg(feature = "alloc")]
109        let wire = wire.to_vec();
110        #[cfg(not(feature = "alloc"))]
111        let wire = {
112            let mut buf = crate::call::Buf::new();
113            if buf.extend_from_slice(wire).is_err() {
114                // Unreachable: `checked_returned_len` already refused what
115                // the `SCRATCH_MAX` scratch could not hold. If it somehow
116                // did, propagate `wire` verbatim, not a truncated copy.
117                crate::contract_op::contract_abort(wire);
118            }
119            buf
120        };
121        Self {
122            selector,
123            wire,
124            payload_start,
125        }
126    }
127
128    #[inline]
129    pub fn selector(&self) -> u64 {
130        self.selector
131    }
132
133    /// The exception payload (everything after the selector prefix).
134    #[inline]
135    pub fn payload(&self) -> &[u8] {
136        &self.wire.as_slice()[self.payload_start..]
137    }
138
139    /// Borrow this owned exception as a transient ref, so `try_as`/`re_throw`
140    /// share their one implementation with [`RawExceptionRef`] instead of
141    /// duplicating it.
142    pub(crate) fn as_ref_exn(&self) -> RawExceptionRef<'_> {
143        RawExceptionRef::from_parts(
144            self.selector,
145            self.wire.as_slice(),
146            self.payload_start,
147        )
148    }
149
150    pub fn try_as<T: Exn + ReadXDROwned>(&self) -> Option<T> {
151        self.as_ref_exn().try_as()
152    }
153
154    /// See [`RawExceptionRef::try_decode`].
155    pub fn try_decode<T: Exn + ReadXDROwned>(
156        &self,
157    ) -> Option<Result<T, ReadError>> {
158        self.as_ref_exn().try_decode()
159    }
160
161    /// Re-throw to our caller with the original wire bytes, verbatim.
162    #[inline]
163    #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
164    pub fn re_throw(&self) -> ! {
165        #[cfg(all(not(target_arch = "riscv32"), feature = "std"))]
166        crate::contract_op::abort_breadcrumb("Re-throwing", "Location", self);
167
168        crate::contract_op::contract_abort(self.wire.as_ref())
169    }
170
171    /// Build from a selector and payload. Test/mock construction.
172    #[cfg(feature = "alloc")]
173    pub fn new(selector: u64, payload: &[u8]) -> Self {
174        let mut wire = crate::internal::encode_xdr(&selector)
175            .expect("u64 CXS-encode cannot fail under alloc");
176        let payload_start = wire.len();
177        wire.extend_from_slice(payload);
178        Self {
179            selector,
180            wire,
181            payload_start,
182        }
183    }
184
185    /// Parse an abort wire buffer (`[CXS(selector)][payload]`) into an owned
186    /// exception. Host tests and mock plumbing.
187    #[cfg(feature = "alloc")]
188    pub fn from_abort_buf(bytes: &[u8]) -> Result<Self, LibraryExn> {
189        let exn = RawExceptionRef::parse(bytes).map_err(|e| {
190            LibraryExn::Calling(CallingError::DecodingException(
191                FaultString::from_display(e),
192            ))
193        })?;
194        Ok(exn.to_owned())
195    }
196}
197
198impl PartialEq for RawException {
199    fn eq(&self, other: &Self) -> bool {
200        self.selector == other.selector && self.payload() == other.payload()
201    }
202}
203
204impl core::fmt::Debug for RawException {
205    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
206        fmt_exn(f, "RawException", self.selector, self.payload())
207    }
208}
209
210impl crate::re_throw::ReThrow for RawException {
211    #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
212    fn re_throw(self) -> ! {
213        RawException::re_throw(&self)
214    }
215}
216
217#[cfg(all(test, feature = "alloc"))]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn parse_extracts_selector_and_payload() {
223        // wire = [CXS(u64 selector)][payload]
224        let mut wire = crate::internal::encode_xdr(&0xABCDu64).unwrap();
225        wire.extend_from_slice(b"payload-bytes");
226
227        let exn = RawExceptionRef::parse(&wire).unwrap();
228
229        assert_eq!(exn.selector(), 0xABCD);
230        assert_eq!(exn.payload(), b"payload-bytes");
231    }
232
233    #[test]
234    fn parse_rejects_garbage() {
235        // Empty bytes cannot possibly contain a CXS-encoded u64 selector.
236        assert!(RawExceptionRef::parse(&[]).is_err());
237    }
238
239    #[test]
240    fn new_roundtrips_selector_and_payload() {
241        let c = RawException::new(0x1234, b"hello");
242        assert_eq!(c.selector(), 0x1234);
243        assert_eq!(c.payload(), b"hello");
244    }
245
246    #[test]
247    fn from_abort_buf_parses_wire_format() {
248        // wire = [CXS(u64 selector)][payload]
249        let mut wire = crate::internal::encode_xdr(&0xABCDu64).unwrap();
250        wire.extend_from_slice(b"payload-bytes");
251        let c = RawException::from_abort_buf(&wire).unwrap();
252        assert_eq!(c.selector(), 0xABCD);
253        assert_eq!(c.payload(), b"payload-bytes");
254    }
255
256    #[test]
257    fn holds_oversized_payload_without_truncation_under_alloc() {
258        let big = [0xABu8; crate::internal::SCRATCH_MAX + 1];
259        let c = RawException::new(0x1234, &big);
260        assert_eq!(c.payload(), &big[..]);
261    }
262
263    #[test]
264    fn eq_compares_selector_and_payload_bytes() {
265        assert_eq!(RawException::new(1, b"x"), RawException::new(1, b"x"));
266        assert_ne!(RawException::new(1, b"x"), RawException::new(2, b"x"));
267        assert_ne!(RawException::new(1, b"x"), RawException::new(1, b"y"));
268    }
269
270    #[test]
271    fn try_as_decodes_matching_selector_and_payload() {
272        let payload =
273            crate::internal::encode_xdr(&LibraryExn::NonNegativeIntOverflow)
274                .unwrap();
275        let c = RawException::new(LibraryExn::SELECTOR, &payload);
276
277        assert_eq!(
278            c.try_as::<LibraryExn>(),
279            Some(LibraryExn::NonNegativeIntOverflow)
280        );
281    }
282
283    #[test]
284    fn try_as_returns_none_for_mismatched_selector() {
285        let payload =
286            crate::internal::encode_xdr(&LibraryExn::NonNegativeIntOverflow)
287                .unwrap();
288        // A valid payload for LibraryExn, but tagged with the wrong selector.
289        let c = RawException::new(!LibraryExn::SELECTOR, &payload);
290
291        assert_eq!(c.try_as::<LibraryExn>(), None);
292    }
293
294    #[test]
295    fn try_as_returns_none_for_undecodable_payload() {
296        let payload =
297            crate::internal::encode_xdr(&LibraryExn::NonNegativeIntOverflow)
298                .unwrap();
299        // Truncate so the discriminant itself can't be read back.
300        let truncated = &payload[..payload.len().saturating_sub(1)];
301        let c = RawException::new(LibraryExn::SELECTOR, truncated);
302
303        assert_eq!(c.try_as::<LibraryExn>(), None);
304    }
305}