iroh_docs/
ticket.rs

1//! Tickets for [`iroh-docs`] documents.
2
3use iroh::NodeAddr;
4use iroh_base::ticket;
5use serde::{Deserialize, Serialize};
6
7use crate::Capability;
8
9/// Contains both a key (either secret or public) to a document, and a list of peers to join.
10#[derive(Serialize, Deserialize, Clone, Debug, derive_more::Display)]
11#[display("{}", ticket::Ticket::serialize(self))]
12pub struct DocTicket {
13    /// either a public or private key
14    pub capability: Capability,
15    /// A list of nodes to contact.
16    pub nodes: Vec<NodeAddr>,
17}
18
19/// Wire format for [`DocTicket`].
20///
21/// In the future we might have multiple variants (not versions, since they
22/// might be both equally valid), so this is a single variant enum to force
23/// postcard to add a discriminator.
24#[derive(Serialize, Deserialize)]
25enum TicketWireFormat {
26    Variant0(DocTicket),
27}
28
29impl ticket::Ticket for DocTicket {
30    const KIND: &'static str = "doc";
31
32    fn to_bytes(&self) -> Vec<u8> {
33        let data = TicketWireFormat::Variant0(self.clone());
34        postcard::to_stdvec(&data).expect("postcard serialization failed")
35    }
36
37    fn from_bytes(bytes: &[u8]) -> Result<Self, ticket::ParseError> {
38        let res: TicketWireFormat = postcard::from_bytes(bytes)?;
39        let TicketWireFormat::Variant0(res) = res;
40        if res.nodes.is_empty() {
41            return Err(ticket::ParseError::verification_failed(
42                "addressing info cannot be empty",
43            ));
44        }
45        Ok(res)
46    }
47}
48
49impl DocTicket {
50    /// Create a new doc ticket
51    pub fn new(capability: Capability, peers: Vec<NodeAddr>) -> Self {
52        Self {
53            capability,
54            nodes: peers,
55        }
56    }
57}
58
59impl std::str::FromStr for DocTicket {
60    type Err = ticket::ParseError;
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        ticket::Ticket::deserialize(s)
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use std::str::FromStr;
69
70    use anyhow::{ensure, Context, Result};
71    use iroh::PublicKey;
72
73    use super::*;
74    use crate::NamespaceId;
75
76    #[test]
77    fn test_ticket_base32() {
78        let node_id =
79            PublicKey::from_str("ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6")
80                .unwrap();
81        let namespace_id = NamespaceId::from(
82            &<[u8; 32]>::try_from(
83                hex::decode("ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6")
84                    .unwrap(),
85            )
86            .unwrap(),
87        );
88
89        let ticket = DocTicket {
90            capability: Capability::Read(namespace_id),
91            nodes: vec![NodeAddr::from_parts(node_id, None, [])],
92        };
93        let s = ticket.to_string();
94        let base32 = data_encoding::BASE32_NOPAD
95            .decode(
96                s.strip_prefix("doc")
97                    .unwrap()
98                    .to_ascii_uppercase()
99                    .as_bytes(),
100            )
101            .unwrap();
102        let expected = parse_hexdump("
103            00 # variant
104            01 # capability discriminator, 1 = read
105            ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6 # namespace id, 32 bytes, see above
106            01 # one node
107            ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6 # node id, 32 bytes, see above
108            00 # no relay url
109            00 # no direct addresses
110        ").unwrap();
111        assert_eq!(base32, expected);
112    }
113
114    /// Parses a commented multi line hexdump into a vector of bytes.
115    ///
116    /// This is useful to write wire level protocol tests.
117    pub fn parse_hexdump(s: &str) -> Result<Vec<u8>> {
118        let mut result = Vec::new();
119
120        for (line_number, line) in s.lines().enumerate() {
121            let data_part = line.split('#').next().unwrap_or("");
122            let cleaned: String = data_part.chars().filter(|c| !c.is_whitespace()).collect();
123
124            ensure!(
125                cleaned.len() % 2 == 0,
126                "Non-even number of hex chars detected on line {}.",
127                line_number + 1
128            );
129
130            for i in (0..cleaned.len()).step_by(2) {
131                let byte_str = &cleaned[i..i + 2];
132                let byte = u8::from_str_radix(byte_str, 16)
133                    .with_context(|| format!("Invalid hex data on line {}.", line_number + 1))?;
134
135                result.push(byte);
136            }
137        }
138
139        Ok(result)
140    }
141}