iroh_tickets/lib.rs
1//! Tickets is a serializable object combining information required for an operation.
2//! Typically tickets contain all information required for an operation, e.g. an iroh blob
3//! ticket would contain the hash of the data as well as information about how to reach the
4//! provider.
5
6use nested_enum_utils::common_fields;
7use snafu::{Backtrace, Snafu};
8
9pub mod endpoint;
10
11/// A ticket is a serializable object combining information required for an operation.
12///
13/// Tickets support serialization to a string using base32 encoding. The kind of
14/// ticket will be prepended to the string to make it somewhat self describing.
15///
16/// Versioning is left to the implementer. Some kinds of tickets might need
17/// versioning, others might not.
18///
19/// The serialization format for converting the ticket from and to bytes is left
20/// to the implementer. We recommend using [postcard] for serialization.
21///
22/// [postcard]: https://docs.rs/postcard/latest/postcard/
23pub trait Ticket: Sized {
24 /// String prefix describing the kind of iroh ticket.
25 ///
26 /// This should be lower case ascii characters.
27 const KIND: &'static str;
28
29 /// Serialize to bytes used in the base32 string representation.
30 fn to_bytes(&self) -> Vec<u8>;
31
32 /// Deserialize from the base32 string representation bytes.
33 fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError>;
34
35 /// Serialize to string.
36 fn serialize(&self) -> String {
37 let mut out = Self::KIND.to_string();
38 data_encoding::BASE32_NOPAD.encode_append(&self.to_bytes(), &mut out);
39 out.to_ascii_lowercase()
40 }
41
42 /// Deserialize from a string.
43 fn deserialize(str: &str) -> Result<Self, ParseError> {
44 let expected = Self::KIND;
45 let Some(rest) = str.strip_prefix(expected) else {
46 return Err(KindSnafu { expected }.build());
47 };
48 let bytes = data_encoding::BASE32_NOPAD.decode(rest.to_ascii_uppercase().as_bytes())?;
49 let ticket = Self::from_bytes(&bytes)?;
50 Ok(ticket)
51 }
52}
53
54/// An error deserializing an iroh ticket.
55#[common_fields({
56 backtrace: Option<Backtrace>,
57 #[snafu(implicit)]
58 span_trace: n0_snafu::SpanTrace,
59})]
60#[derive(Debug, Snafu)]
61#[allow(missing_docs)]
62#[snafu(visibility(pub(crate)))]
63#[non_exhaustive]
64pub enum ParseError {
65 /// Found a ticket with the wrong prefix, indicating the wrong kind.
66 #[snafu(display("wrong prefix, expected {expected}"))]
67 Kind {
68 /// The expected prefix.
69 expected: &'static str,
70 },
71 /// This looks like a ticket, but postcard deserialization failed.
72 #[snafu(transparent)]
73 Postcard { source: postcard::Error },
74 /// This looks like a ticket, but base32 decoding failed.
75 #[snafu(transparent)]
76 Encoding { source: data_encoding::DecodeError },
77 /// Verification of the deserialized bytes failed.
78 #[snafu(display("verification failed: {message}"))]
79 Verify { message: &'static str },
80}
81
82impl ParseError {
83 /// Returns a [`ParseError`] that indicates the given ticket has the wrong
84 /// prefix.
85 ///
86 /// Indicate the expected prefix.
87 pub fn wrong_prefix(expected: &'static str) -> Self {
88 KindSnafu { expected }.build()
89 }
90
91 /// Return a `ParseError` variant that indicates verification of the
92 /// deserialized bytes failed.
93 pub fn verification_failed(message: &'static str) -> Self {
94 VerifySnafu { message }.build()
95 }
96}