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.
5use n0_error::{e, stack_error};
6
7pub mod endpoint;
8
9/// A ticket is a serializable object combining information required for an operation.
10///
11/// Tickets support serialization to a string using base32 encoding. The kind of
12/// ticket will be prepended to the string to make it somewhat self describing.
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/// [postcard]: https://docs.rs/postcard/latest/postcard/
21pub trait Ticket: Sized {
22    /// String prefix describing the kind of iroh ticket.
23    ///
24    /// This should be lower case ascii characters.
25    const KIND: &'static str;
26
27    /// Serialize to bytes used in the base32 string representation.
28    fn to_bytes(&self) -> Vec<u8>;
29
30    /// Deserialize from the base32 string representation bytes.
31    fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError>;
32
33    /// Serialize to string.
34    fn serialize(&self) -> String {
35        let mut out = Self::KIND.to_string();
36        data_encoding::BASE32_NOPAD.encode_append(&self.to_bytes(), &mut out);
37        out.to_ascii_lowercase()
38    }
39
40    /// Deserialize from a string.
41    fn deserialize(str: &str) -> Result<Self, ParseError> {
42        let expected = Self::KIND;
43        let Some(rest) = str.strip_prefix(expected) else {
44            return Err(e!(ParseError::Kind { expected }));
45        };
46        let bytes = data_encoding::BASE32_NOPAD.decode(rest.to_ascii_uppercase().as_bytes())?;
47        let ticket = Self::from_bytes(&bytes)?;
48        Ok(ticket)
49    }
50}
51
52/// An error deserializing an iroh ticket.
53#[stack_error(derive, add_meta, from_sources)]
54#[allow(missing_docs)]
55#[non_exhaustive]
56pub enum ParseError {
57    /// Found a ticket with the wrong prefix, indicating the wrong kind.
58    #[error("wrong prefix, expected {expected}")]
59    Kind {
60        /// The expected prefix.
61        expected: &'static str,
62    },
63    /// This looks like a ticket, but postcard deserialization failed.
64    #[error(transparent)]
65    Postcard {
66        #[error(source, std_err)]
67        source: postcard::Error,
68    },
69    /// This looks like a ticket, but base32 decoding failed.
70    #[error(transparent)]
71    Encoding {
72        #[error(source, std_err)]
73        source: data_encoding::DecodeError,
74    },
75    /// Verification of the deserialized bytes failed.
76    #[error("verification failed: {message}")]
77    Verify { message: &'static str },
78}
79
80impl ParseError {
81    /// Returns a [`ParseError`] that indicates the given ticket has the wrong
82    /// prefix.
83    ///
84    /// Indicate the expected prefix.
85    pub fn wrong_prefix(expected: &'static str) -> Self {
86        e!(ParseError::Kind { expected })
87    }
88
89    /// Return a `ParseError` variant that indicates verification of the
90    /// deserialized bytes failed.
91    pub fn verification_failed(message: &'static str) -> Self {
92        e!(ParseError::Verify { message })
93    }
94}