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(crate) 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(crate) enum IrohAttr {
83 Relay,
85 Addr,
87 UserData,
89}
90
91#[derive(Debug)]
97pub(crate) 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(crate) fn from_parts(
105 endpoint_id: EndpointId,
106 pairs: impl Iterator<Item = (T, String)>,
107 ) -> Self {
108 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
109 for (k, v) in pairs {
110 attrs.entry(k).or_default().push(v);
111 }
112 Self { attrs, endpoint_id }
113 }
114
115 pub(crate) fn from_strings(
117 endpoint_id: EndpointId,
118 strings: impl Iterator<Item = String>,
119 ) -> Result<Self, ParseError> {
120 let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
121 for s in strings {
122 let mut parts = s.split('=');
123 let (Some(key), Some(value)) = (parts.next(), parts.next()) else {
124 return Err(e!(ParseError::UnexpectedFormat { s }));
125 };
126 let attr = T::from_str(key).map_err(|_| {
127 e!(ParseError::AttrFromString {
128 key: key.to_string()
129 })
130 })?;
131 attrs.entry(attr).or_default().push(value.to_string());
132 }
133 Ok(Self { attrs, endpoint_id })
134 }
135
136 pub(crate) fn attrs(&self) -> &BTreeMap<T, Vec<String>> {
138 &self.attrs
139 }
140
141 pub(crate) fn endpoint_id(&self) -> EndpointId {
143 self.endpoint_id
144 }
145
146 pub(crate) fn from_txt_lookup(
151 name: String,
152 lookup: impl Iterator<Item = impl Display>,
153 ) -> Result<Self, ParseError> {
154 let queried_endpoint_id = endpoint_id_from_txt_name(&name)?;
155 let strings = lookup.map(|record| record.to_string());
156 Self::from_strings(queried_endpoint_id, strings)
157 }
158
159 pub(crate) fn from_pkarr_signed_packet(
161 packet: &pkarr::SignedPacket,
162 ) -> Result<Self, ParseError> {
163 let pubkey = packet.public_key();
164 let endpoint_id = EndpointId::from_bytes(pubkey.as_bytes()).expect("valid key");
165 let txt_strs = packet.txt_records(IROH_TXT_NAME);
166 Self::from_strings(endpoint_id, txt_strs.into_iter())
167 }
168
169 pub(crate) fn to_txt_strings(&self) -> impl Iterator<Item = String> + '_ {
171 self.attrs
172 .iter()
173 .flat_map(move |(k, vs)| vs.iter().map(move |v| format!("{k}={v}")))
174 }
175
176 pub(crate) fn to_pkarr_signed_packet(
180 &self,
181 secret_key: &SecretKey,
182 ttl: u32,
183 ) -> Result<pkarr::SignedPacket, EncodingError> {
184 let signed_packet = pkarr::SignedPacket::from_txt_strings(
185 secret_key,
186 IROH_TXT_NAME,
187 self.to_txt_strings(),
188 ttl,
189 )
190 .map_err(|err| e!(EncodingError::FailedBuildingPacket, err))?;
191 Ok(signed_packet)
192 }
193}