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}