1use iroh::EndpointAddr;
4use iroh_tickets::{ParseError, Ticket};
5use serde::{Deserialize, Serialize};
6
7use crate::Capability;
8
9#[derive(Serialize, Deserialize, Clone, Debug, derive_more::Display)]
11#[display("{}", Ticket::serialize(self))]
12pub struct DocTicket {
13 pub capability: Capability,
15 pub nodes: Vec<EndpointAddr>,
17}
18
19#[derive(Serialize, Deserialize)]
25enum TicketWireFormat {
26 Variant0(DocTicket),
27}
28
29impl 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, ParseError> {
38 let res: TicketWireFormat = postcard::from_bytes(bytes)?;
39 let TicketWireFormat::Variant0(res) = res;
40 if res.nodes.is_empty() {
41 return Err(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<EndpointAddr>) -> Self {
52 Self {
53 capability,
54 nodes: peers,
55 }
56 }
57}
58
59impl std::str::FromStr for DocTicket {
60 type Err = ParseError;
61 fn from_str(s: &str) -> Result<Self, Self::Err> {
62 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![EndpointAddr::new(node_id)],
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 addrs
109 ").unwrap();
110 assert_eq!(base32, expected);
111 }
112
113 pub fn parse_hexdump(s: &str) -> Result<Vec<u8>> {
117 let mut result = Vec::new();
118
119 for (line_number, line) in s.lines().enumerate() {
120 let data_part = line.split('#').next().unwrap_or("");
121 let cleaned: String = data_part.chars().filter(|c| !c.is_whitespace()).collect();
122
123 ensure!(
124 cleaned.len() % 2 == 0,
125 "Non-even number of hex chars detected on line {}.",
126 line_number + 1
127 );
128
129 for i in (0..cleaned.len()).step_by(2) {
130 let byte_str = &cleaned[i..i + 2];
131 let byte = u8::from_str_radix(byte_str, 16)
132 .with_context(|| format!("Invalid hex data on line {}.", line_number + 1))?;
133
134 result.push(byte);
135 }
136 }
137
138 Ok(result)
139 }
140}