1use iroh::NodeAddr;
4use iroh_base::ticket;
5use serde::{Deserialize, Serialize};
6
7use crate::Capability;
8
9#[derive(Serialize, Deserialize, Clone, Debug, derive_more::Display)]
11#[display("{}", ticket::Ticket::serialize(self))]
12pub struct DocTicket {
13 pub capability: Capability,
15 pub nodes: Vec<NodeAddr>,
17}
18
19#[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 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 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}