Skip to main content

thruput_exn/
lib.rs

1//! Exception trait and the borrowed exception type, split out of the SDK
2//! (like `thruput-event`) so host-side tooling below the SDK can decode
3//! thrown exceptions.
4
5#![no_std]
6
7use core::fmt::Debug;
8use xdrs::{ReadError, ReadXDR, ReadXDROwned, WriteXDR, cxs};
9
10/// A trait for exception types that can be serialized across contract calls.
11///
12/// Each implementing type gets a unique u64 selector derived from its
13/// canonical type signature (via `#[exn]`). The selector is used
14/// for dispatch when deserializing exceptions on the client side.
15///
16/// Throwing only needs `WriteXDR`, so decoding is not a supertrait; the
17/// generic decode sites ([`RawExceptionRef::try_as`] and the SDK's
18/// `RawException::try_as`) require `ReadXDROwned` explicitly.
19#[diagnostic::on_unimplemented(
20    message = "`{Self}` is not a contract exception",
21    label = "not a contract exception",
22    note = "annotate `{Self}` with `#[contract_exception]` (or `#[exn]` if \
23            you handle your own derives); `throws = ...` in \
24            `#[contract(...)]` requires it"
25)]
26pub trait Exn: Debug + PartialEq + WriteXDR {
27    const SELECTOR: u64;
28}
29
30/// A reference throws as its referent: same selector, same wire encoding
31/// (`WriteXDR for &T` writes `T`). Lets throw paths borrow an exception
32/// instead of cloning it.
33impl<T: Exn> Exn for &T {
34    const SELECTOR: u64 = T::SELECTOR;
35}
36
37/// Exn impl for Infallible: this type can never be instantiated,
38/// so its selector is 0 (a reserved/unused value).
39impl Exn for core::convert::Infallible {
40    const SELECTOR: u64 = 0;
41}
42
43/// A raw exception borrowed from an abort buffer, `[CXS(selector)][payload]`.
44///
45/// In the SDK this only exists inside the `on_exn` dispatch closure of
46/// `try_call_raw_with`, borrowing the call scratch for exactly the closure's
47/// lifetime; `RawException` is its owned form. On the host, the mock
48/// executor parses one straight from a frame's abort buffer.
49///
50/// `Copy`: all three fields are, and `MatchExn` hands out reborrows of it.
51/// Covariance in `'s` makes a copy usable at any shorter lifetime.
52#[derive(Clone, Copy)]
53pub struct RawExceptionRef<'s> {
54    selector: u64,
55    /// Full verbatim abort wire bytes: `[CXS(selector)][payload]`.
56    wire: &'s [u8],
57    payload_start: usize,
58}
59
60impl<'s> RawExceptionRef<'s> {
61    /// Fails only when the buffer does not start with a CXS-encoded `u64`:
62    /// the frame aborted with something other than a contract exception.
63    pub fn parse(wire: &'s [u8]) -> Result<Self, ReadError> {
64        let mut reader = cxs::buf_reader::BufReader::new(wire);
65        let selector: u64 = ReadXDR::read_xdr(&mut reader)?;
66        Ok(Self {
67            selector,
68            wire,
69            payload_start: wire.len() - reader.remaining_slice().len(),
70        })
71    }
72
73    /// Reassemble from parts an earlier [`parse`](Self::parse) produced, for
74    /// an owner of a copied wire buffer that kept `selector` and
75    /// `payload_start` alongside it. The SDK's owned `RawException` is the
76    /// intended caller; the offset is only checked in debug builds.
77    #[doc(hidden)]
78    pub fn from_parts(
79        selector: u64,
80        wire: &'s [u8],
81        payload_start: usize,
82    ) -> Self {
83        debug_assert!(
84            payload_start <= wire.len(),
85            "payload_start {payload_start} past wire end {}",
86            wire.len()
87        );
88        Self {
89            selector,
90            wire,
91            payload_start,
92        }
93    }
94
95    pub fn selector(&self) -> u64 {
96        self.selector
97    }
98
99    /// The full abort wire bytes, selector prefix included.
100    pub fn wire(&self) -> &'s [u8] {
101        self.wire
102    }
103
104    pub fn payload(&self) -> &'s [u8] {
105        &self.wire[self.payload_start..]
106    }
107
108    /// Where [`payload`](Self::payload) starts within [`wire`](Self::wire).
109    pub fn payload_start(&self) -> usize {
110        self.payload_start
111    }
112
113    /// Detach into an owned exception (one copy). Which owned type is up to
114    /// the caller's context; the SDK's `RawException` is the one that
115    /// exists, and this resolves through its `From` impl.
116    pub fn to_owned<O: From<Self>>(self) -> O {
117        O::from(self)
118    }
119
120    /// Decode as `T` if the selector matches, `None` otherwise.
121    ///
122    /// `None` covers two different situations, a selector belonging to some
123    /// other type, and this type's selector over a payload that will not
124    /// decode. Use [`try_decode`](Self::try_decode) when they need telling
125    /// apart.
126    ///
127    /// `T` must contain no auth-token handle (the SDK's `Signed`): abort
128    /// frames carry none, so one would decode to the uninitialized
129    /// `u32::MAX` sentinel, a value shaped like an auth token that faults on
130    /// use. Nothing is checked here; spec canonicalization rejects such a
131    /// payload when the contract is built.
132    pub fn try_as<T: Exn + ReadXDROwned>(&self) -> Option<T> {
133        // Defined in terms of `try_decode` so the two cannot drift on which
134        // payloads they accept.
135        self.try_decode().and_then(|decoded| decoded.ok())
136    }
137
138    /// [`try_as`](Self::try_as) with the decode failure kept separate from
139    /// the selector mismatch:
140    ///
141    /// - `None`: the selector belongs to some other exception type.
142    /// - `Some(Ok(t))`: the selector matched and the payload decoded.
143    /// - `Some(Err(e))`: the selector matched, but the payload does not
144    ///   decode as `T`.
145    ///
146    /// The third case is not only a forged or truncated payload. An
147    /// exception's selector is `sha256(major_version :: crate :: type)` and
148    /// says nothing about the payload's layout, so a callee that adds an
149    /// enum variant (a semver-compatible change that leaves the selector
150    /// untouched) throws something an older caller cannot decode. Since
151    /// [`try_as`] reports that as `None`, indistinguishable from an
152    /// unrelated error type, a caller that must not confuse the two should
153    /// reach for this instead.
154    ///
155    /// Note that decoding reads a *prefix*: a payload carrying extra
156    /// trailing bytes decodes as the shorter shape and is reported as
157    /// `Some(Ok(..))`. Appending a field is therefore accepted silently
158    /// rather than surfacing here.
159    ///
160    /// [`try_as`]: Self::try_as
161    pub fn try_decode<T: Exn + ReadXDROwned>(
162        &self,
163    ) -> Option<Result<T, ReadError>> {
164        if self.selector != T::SELECTOR {
165            return None;
166        }
167        let mut reader = cxs::buf_reader::BufReader::new(self.payload());
168        Some(ReadXDR::read_xdr(&mut reader))
169    }
170}
171
172impl core::fmt::Debug for RawExceptionRef<'_> {
173    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
174        let payload = self.payload();
175        write!(
176            f,
177            "RawExceptionRef {{ selector: 0x{:016X}, payload: {} bytes",
178            self.selector,
179            payload.len(),
180        )?;
181        #[cfg(not(target_arch = "riscv32"))]
182        {
183            write!(f, ", hex: ")?;
184            for b in payload {
185                write!(f, "{b:02x}")?;
186            }
187        }
188        write!(f, " }}")
189    }
190}
191
192#[cfg(test)]
193extern crate std;
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use std::vec::Vec;
199
200    #[derive(Debug, PartialEq, xdrs::WriteXDR, xdrs::ReadXDR)]
201    struct TestExn(u32);
202
203    impl Exn for TestExn {
204        const SELECTOR: u64 = 0xABCD;
205    }
206
207    fn wire(selector: u64, payload: &[u8]) -> Vec<u8> {
208        let mut wire = cxs::encode(&selector).unwrap();
209        wire.extend_from_slice(payload);
210        wire
211    }
212
213    #[test]
214    fn parse_separates_selector_and_payload() {
215        let wire = wire(0xABCD, b"payload-bytes");
216        let exn = RawExceptionRef::parse(&wire).unwrap();
217        assert_eq!(exn.selector(), 0xABCD);
218        assert_eq!(exn.payload(), b"payload-bytes");
219        assert_eq!(exn.wire(), &wire[..]);
220        assert_eq!(exn.payload_start(), wire.len() - b"payload-bytes".len());
221    }
222
223    #[test]
224    fn parse_rejects_garbage() {
225        assert!(RawExceptionRef::parse(&[]).is_err());
226    }
227
228    #[test]
229    fn try_decode_distinguishes_selector_from_payload_failures() {
230        let payload = cxs::encode(&7u32).unwrap();
231        let ok = wire(TestExn::SELECTOR, &payload);
232        assert!(matches!(
233            RawExceptionRef::parse(&ok).unwrap().try_decode::<TestExn>(),
234            Some(Ok(TestExn(7)))
235        ));
236        assert_eq!(
237            RawExceptionRef::parse(&ok).unwrap().try_as::<TestExn>(),
238            Some(TestExn(7))
239        );
240
241        let other = wire(TestExn::SELECTOR + 1, &payload);
242        assert!(
243            RawExceptionRef::parse(&other)
244                .unwrap()
245                .try_decode::<TestExn>()
246                .is_none()
247        );
248
249        let truncated = wire(TestExn::SELECTOR, &[]);
250        assert!(matches!(
251            RawExceptionRef::parse(&truncated)
252                .unwrap()
253                .try_decode::<TestExn>(),
254            Some(Err(_))
255        ));
256        assert_eq!(
257            RawExceptionRef::parse(&truncated)
258                .unwrap()
259                .try_as::<TestExn>(),
260            None
261        );
262    }
263}