use std::{collections::BTreeMap, fmt::Display, hash::Hash, str::FromStr};
use iroh_base::{EndpointId, SecretKey};
use n0_error::{e, stack_error};
use crate::pkarr;
pub const IROH_TXT_NAME: &str = "_iroh";
#[allow(missing_docs)]
#[stack_error(derive, add_meta)]
#[non_exhaustive]
pub enum EncodingError {
#[error(transparent)]
FailedBuildingPacket {
#[error(std_err)]
source: pkarr::SignedPacketBuildError,
},
}
#[allow(missing_docs)]
#[stack_error(derive, add_meta, from_sources)]
#[non_exhaustive]
pub enum ParseError {
#[error("Expected format `key=value`, received `{s}`")]
UnexpectedFormat { s: String },
#[error("Could not convert key to Attr")]
AttrFromString { key: String },
#[error("Expected 2 labels, received {num_labels}")]
NumLabels { num_labels: usize },
#[error("Could not parse labels")]
Utf8 {
#[error(std_err)]
source: std::str::Utf8Error,
},
#[error("Record is not an `iroh` record, expected `_iroh`, got `{label}`")]
NotAnIrohRecord { label: String },
#[error(transparent)]
DecodingError { source: iroh_base::KeyParsingError },
}
pub fn endpoint_id_from_txt_name(name: &str) -> Result<EndpointId, ParseError> {
let num_labels = name.split(".").count();
if num_labels < 2 {
return Err(e!(ParseError::NumLabels { num_labels }));
}
let mut labels = name.split(".");
let label = labels.next().expect("checked above");
if label != IROH_TXT_NAME {
return Err(e!(ParseError::NotAnIrohRecord {
label: label.to_string()
}));
}
let label = labels.next().expect("checked above");
let endpoint_id = EndpointId::from_z32(label)?;
Ok(endpoint_id)
}
#[derive(
Debug, strum::Display, strum::AsRefStr, strum::EnumString, Hash, Eq, PartialEq, Ord, PartialOrd,
)]
#[strum(serialize_all = "kebab-case")]
pub enum IrohAttr {
Relay,
Addr,
UserData,
}
#[derive(Debug)]
pub struct TxtAttrs<T> {
endpoint_id: EndpointId,
attrs: BTreeMap<T, Vec<String>>,
}
impl<T: FromStr + Display + Hash + Ord> TxtAttrs<T> {
pub fn from_parts(endpoint_id: EndpointId, pairs: impl Iterator<Item = (T, String)>) -> Self {
let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
for (k, v) in pairs {
attrs.entry(k).or_default().push(v);
}
Self { attrs, endpoint_id }
}
pub fn from_strings(
endpoint_id: EndpointId,
strings: impl Iterator<Item = String>,
) -> Result<Self, ParseError> {
let mut attrs: BTreeMap<T, Vec<String>> = BTreeMap::new();
for s in strings {
let mut parts = s.split('=');
let (Some(key), Some(value)) = (parts.next(), parts.next()) else {
return Err(e!(ParseError::UnexpectedFormat { s }));
};
let attr = T::from_str(key).map_err(|_| {
e!(ParseError::AttrFromString {
key: key.to_string()
})
})?;
attrs.entry(attr).or_default().push(value.to_string());
}
Ok(Self { attrs, endpoint_id })
}
pub fn attrs(&self) -> &BTreeMap<T, Vec<String>> {
&self.attrs
}
pub fn endpoint_id(&self) -> EndpointId {
self.endpoint_id
}
pub fn from_txt_lookup(
name: String,
lookup: impl Iterator<Item = impl Display>,
) -> Result<Self, ParseError> {
let queried_endpoint_id = endpoint_id_from_txt_name(&name)?;
let strings = lookup.map(|record| record.to_string());
Self::from_strings(queried_endpoint_id, strings)
}
pub fn from_pkarr_signed_packet(packet: &pkarr::SignedPacket) -> Result<Self, ParseError> {
let pubkey = packet.public_key();
let endpoint_id = EndpointId::from_bytes(pubkey.as_bytes()).expect("valid key");
let txt_strs = packet.txt_records(IROH_TXT_NAME);
Self::from_strings(endpoint_id, txt_strs.into_iter())
}
pub fn to_txt_strings(&self) -> impl Iterator<Item = String> + '_ {
self.attrs
.iter()
.flat_map(move |(k, vs)| vs.iter().map(move |v| format!("{k}={v}")))
}
pub fn to_pkarr_signed_packet(
&self,
secret_key: &SecretKey,
ttl: u32,
) -> Result<pkarr::SignedPacket, EncodingError> {
let signed_packet = pkarr::SignedPacket::from_txt_strings(
secret_key,
IROH_TXT_NAME,
self.to_txt_strings(),
ttl,
)
.map_err(|err| e!(EncodingError::FailedBuildingPacket, err))?;
Ok(signed_packet)
}
}