1use std::{collections::BTreeMap, fmt::Display, hash::Hash, str::FromStr};
13
14use iroh_base::{EndpointId, SecretKey};
15use n0_error::{e, stack_error};
16
17use crate::pkarr;
18
19pub const IROH_TXT_NAME: &str = "_iroh";
21
22#[allow(missing_docs)]
23#[stack_error(derive, add_meta)]
24#[non_exhaustive]
25pub enum EncodingError {
26 #[error(transparent)]
27 FailedBuildingPacket {
28 #[error(std_err)]
29 source: pkarr::SignedPacketBuildError,
30 },
31}
32
33#[allow(missing_docs)]
34#[stack_error(derive, add_meta, from_sources)]
35#[non_exhaustive]
36pub enum ParseError {
37 #[error("Expected format `key=value`, received `{s}`")]
38 UnexpectedFormat { s: String },
39 #[error("Could not convert key to Attr")]
40 AttrFromString { key: String },
41 #[error("Expected 2 labels, received {num_labels}")]
42 NumLabels { num_labels: usize },
43 #[error("Could not parse labels")]
44 Utf8 {
45 #[error(std_err)]
46 source: std::str::Utf8Error,
47 },
48 #[error("Record is not an `iroh` record, expected `_iroh`, got `{label}`")]
49 NotAnIrohRecord { label: String },
50 #[error(transparent)]
51 DecodingError { source: iroh_base::KeyParsingError },
52}
53
54pub fn endpoint_id_from_txt_name(name: &str) -> Result<EndpointId, ParseError> {
59 let num_labels = name.split(".").count();
60 if num_labels < 2 {
61 return Err(e!(ParseError::NumLabels { num_labels }));
62 }
63 let mut labels = name.split(".");
64 let label = labels.next().expect("checked above");
65 if label != IROH_TXT_NAME {
66 return Err(e!(ParseError::NotAnIrohRecord {
67 label: label.to_string()
68 }));
69 }
70 let label = labels.next().expect("checked above");
71 let endpoint_id = EndpointId::from_z32(label)?;
72 Ok(endpoint_id)
73}
74
75#[derive(
79 Debug, strum::Display, strum::AsRefStr, strum::EnumString, Hash, Eq, PartialEq, Ord, PartialOrd,
80)]
81#[strum(serialize_all = "kebab-case")]
82pub enum IrohAttr {
83 Relay,
85 Addr,
87 UserData,
89}
90
91#[derive(Debug)]
97pub struct TxtAttrs<T> {
98 endpoint_id: EndpointId,
99 attrs: BTreeMap<T, Vec<String>>,
100}
101
102impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
103 pub fn from_parts(endpoint_id: EndpointId, pairs: impl Iterator<Item = (T, String)>) -> Self {
105 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
106 for (k, v) in pairs {
107 attrs.entry(k).or_default().push(v);
108 }
109 Self { attrs, endpoint_id }
110 }
111
112 pub fn from_strings(
114 endpoint_id: EndpointId,
115 strings: impl Iterator<Item = String>,
116 ) -> Result<Self, ParseError> {
117 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
118 for s in strings {
119 let mut parts = s.split('=');
120 let (Some(key), Some(value)) = (parts.next(), parts.next()) else {
121 return Err(e!(ParseError::UnexpectedFormat { s }));
122 };
123 let attr = T::from_str(key).map_err(|_| {
124 e!(ParseError::AttrFromString {
125 key: key.to_string()
126 })
127 })?;
128 attrs.entry(attr).or_default().push(value.to_string());
129 }
130 Ok(Self { attrs, endpoint_id })
131 }
132
133 pub fn attrs(&self) -> &BTreeMap<T, Vec<String>> {
135 &self.attrs
136 }
137
138 pub fn endpoint_id(&self) -> EndpointId {
140 self.endpoint_id
141 }
142
143 pub fn from_txt_lookup(
148 name: String,
149 lookup: impl Iterator<Item = impl Display>,
150 ) -> Result<Self, ParseError> {
151 let queried_endpoint_id = endpoint_id_from_txt_name(&name)?;
152 let strings = lookup.map(|record| record.to_string());
153 Self::from_strings(queried_endpoint_id, strings)
154 }
155
156 pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self, ParseError> {
158 let pubkey = packet.public_key();
159 let endpoint_id = EndpointId::from_bytes(pubkey.as_bytes()).expect("valid key");
160 let txt_strs = packet.txt_records(IROH_TXT_NAME);
161 Self::from_strings(endpoint_id, txt_strs.into_iter())
162 }
163
164 pub fn to_txt_strings(&self) -> impl Iterator<Item = String> + '_ {
166 self.attrs
167 .iter()
168 .flat_map(move |(k, vs)| vs.iter().map(move |v| format!("{k}={v}")))
169 }
170
171 pub fn to_pkarr_signed_packet(
175 &self,
176 secret_key: &SecretKey,
177 ttl: u32,
178 ) -> Result<pkarr::SignedPacket, EncodingError> {
179 let signed_packet = pkarr::SignedPacket::from_txt_strings(
180 secret_key,
181 IROH_TXT_NAME,
182 self.to_txt_strings(),
183 ttl,
184 )
185 .map_err(|err| e!(EncodingError::FailedBuildingPacket, err))?;
186 Ok(signed_packet)
187 }
188}