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}