iroh_tickets/lib.rs
1#![doc = include_str!("../README.md")]
2
3use n0_error::{e, stack_error};
4
5pub mod endpoint;
6
7/// A ticket is a serializable object combining information required for an operation.
8///
9/// Tickets are convertible to and from a byte representation via [`encode_bytes`] /
10/// [`decode_bytes`], and to and from a canonical string form (the lowercase [`KIND`]
11/// prefix followed by base32 of the bytes) via [`encode_string`] / [`decode_string`].
12/// Implementers only need to provide [`KIND`], [`encode_bytes`], and [`decode_bytes`].
13///
14/// Versioning is left to the implementer. Some kinds of tickets might need
15/// versioning, others might not.
16///
17/// The serialization format for converting the ticket from and to bytes is left
18/// to the implementer. We recommend using [postcard] for serialization.
19///
20/// [`KIND`]: Ticket::KIND
21/// [`encode_bytes`]: Ticket::encode_bytes
22/// [`decode_bytes`]: Ticket::decode_bytes
23/// [`encode_string`]: Ticket::encode_string
24/// [`decode_string`]: Ticket::decode_string
25/// [postcard]: https://docs.rs/postcard/latest/postcard/
26pub trait Ticket: Sized {
27 /// String prefix describing the kind of iroh ticket.
28 ///
29 /// This should be lower case ascii characters.
30 const KIND: &'static str;
31
32 /// Encode the ticket into its byte representation.
33 fn encode_bytes(&self) -> Vec<u8>;
34
35 /// Decode a ticket from its byte representation.
36 fn decode_bytes(bytes: &[u8]) -> Result<Self, ParseError>;
37
38 /// Encode the ticket into its canonical string form.
39 ///
40 /// The default implementation produces the lowercase [`KIND`](Self::KIND) prefix
41 /// followed by base32 (no padding) of [`encode_bytes`](Self::encode_bytes).
42 /// Implementers may override this to use a different string encoding, in which
43 /// case [`decode_string`](Self::decode_string) must be overridden to match.
44 fn encode_string(&self) -> String {
45 let mut out = Self::KIND.to_string();
46 data_encoding::BASE32_NOPAD.encode_append(&self.encode_bytes(), &mut out);
47 out.make_ascii_lowercase();
48 out
49 }
50
51 /// Decode a ticket from its canonical string form.
52 ///
53 /// The default implementation expects the lowercase [`KIND`](Self::KIND) prefix
54 /// followed by base32 (no padding) of the bytes accepted by
55 /// [`decode_bytes`](Self::decode_bytes). Implementers that override
56 /// [`encode_string`](Self::encode_string) must override this to match.
57 fn decode_string(s: &str) -> Result<Self, ParseError> {
58 let expected = Self::KIND;
59 let Some(rest) = s.strip_prefix(expected) else {
60 return Err(e!(ParseError::Kind { expected }));
61 };
62 let bytes = data_encoding::BASE32_NOPAD.decode(rest.to_ascii_uppercase().as_bytes())?;
63 Self::decode_bytes(&bytes)
64 }
65}
66
67/// An error deserializing an iroh ticket.
68#[stack_error(derive, add_meta, from_sources)]
69#[allow(missing_docs)]
70#[non_exhaustive]
71pub enum ParseError {
72 /// Found a ticket with the wrong prefix, indicating the wrong kind.
73 #[error("wrong prefix, expected {expected}")]
74 Kind {
75 /// The expected prefix.
76 expected: &'static str,
77 },
78 /// This looks like a ticket, but postcard deserialization failed.
79 #[error(transparent)]
80 Postcard {
81 #[error(source, std_err)]
82 source: postcard::Error,
83 },
84 /// This looks like a ticket, but base32 decoding failed.
85 #[error(transparent)]
86 Encoding {
87 #[error(source, std_err)]
88 source: data_encoding::DecodeError,
89 },
90 /// Verification of the deserialized bytes failed.
91 #[error("verification failed: {message}")]
92 Verify { message: &'static str },
93}
94
95impl ParseError {
96 /// Returns a [`ParseError`] that indicates the given ticket has the wrong
97 /// prefix.
98 ///
99 /// Indicate the expected prefix.
100 pub fn wrong_prefix(expected: &'static str) -> Self {
101 e!(ParseError::Kind { expected })
102 }
103
104 /// Return a `ParseError` variant that indicates verification of the
105 /// deserialized bytes failed.
106 pub fn verification_failed(message: &'static str) -> Self {
107 e!(ParseError::Verify { message })
108 }
109}