use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use base32;
const SUFFIX_I2P: &str = ".i2p";
const SUFFIX_I2P_ALT: &str = ".i2p.alt";
pub fn ends_with_suffix(s: &str) -> bool { s.ends_with(SUFFIX_I2P) || s.ends_with(SUFFIX_I2P_ALT) }
const SUFFIX_B32_I2P: &str = ".b32.i2p";
const SUFFIX_B32_I2P_ALT: &str = ".b32.i2p.alt";
const ALPHABET: base32::Alphabet = base32::Alphabet::RFC4648 { padding: false };
const B32_LEN_BYTES: usize = 32;
const B32_LEN_CHARS: usize = 52;
const EXT_B32_CHECKSUM_BYTES: usize = 3;
const EXT_B32_MIN_LEN_CHARS: usize = 56;
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum I2pAddr {
Base32 {
digest: [u8; B32_LEN_BYTES],
alt: bool,
},
ExtendedBase32 {
checksum: [u8; EXT_B32_CHECKSUM_BYTES],
payload: Vec<u8>,
alt: bool,
},
Name {
name: String,
alt: bool,
},
}
#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[display(doc_comments)]
#[non_exhaustive]
pub enum I2pAddrParseError {
NoSuffix(String),
InvalidBase32(String),
InvalidLen(String),
}
impl I2pAddr {
fn try_from_b32(stripped: &str, alt: bool) -> Result<Self, I2pAddrParseError> {
if stripped.len() == B32_LEN_CHARS {
let Some(digest) = base32::decode(ALPHABET, stripped) else {
return Err(I2pAddrParseError::InvalidBase32(stripped.to_owned()));
};
if digest.len() != B32_LEN_BYTES {
return Err(I2pAddrParseError::InvalidLen(stripped.to_owned()));
}
match digest.try_into() {
Ok(digest) => Ok(Self::Base32 { digest, alt }),
Err(_) => unreachable!("digest length is checked"),
}
} else if stripped.len() >= EXT_B32_MIN_LEN_CHARS {
let Some(decoded) = base32::decode(ALPHABET, stripped) else {
return Err(I2pAddrParseError::InvalidBase32(stripped.to_owned()));
};
let (checksum, payload) = decoded.split_at(EXT_B32_CHECKSUM_BYTES);
Ok(Self::ExtendedBase32 {
checksum: checksum
.try_into()
.unwrap_or_else(|_| unreachable!("checksum length is guaranteed by split_at")),
payload: payload.to_vec(),
alt,
})
} else {
Err(I2pAddrParseError::InvalidLen(stripped.to_owned()))
}
}
fn prefix(&self) -> String {
match self {
Self::Base32 { digest, .. } => {
base32::encode(ALPHABET, digest.as_slice()).to_lowercase()
}
Self::ExtendedBase32 {
checksum, payload, ..
} => base32::encode(ALPHABET, &[checksum.as_slice(), payload.as_slice()].concat())
.to_lowercase(),
Self::Name { name, .. } => name.clone(),
}
}
fn suffix(&self) -> &'static str {
match self {
Self::Base32 { alt, .. } | Self::ExtendedBase32 { alt, .. } => {
if *alt {
SUFFIX_B32_I2P_ALT
} else {
SUFFIX_B32_I2P
}
}
Self::Name { alt, .. } => {
if *alt {
SUFFIX_I2P_ALT
} else {
SUFFIX_I2P
}
}
}
}
}
impl FromStr for I2pAddr {
type Err = I2pAddrParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(stripped) = s.strip_suffix(SUFFIX_B32_I2P) {
Self::try_from_b32(stripped, false)
} else if let Some(stripped) = s.strip_suffix(SUFFIX_B32_I2P_ALT) {
Self::try_from_b32(stripped, true)
} else if let Some(stripped) = s.strip_suffix(SUFFIX_I2P) {
Ok(Self::Name {
name: stripped.to_owned(),
alt: false,
})
} else if let Some(stripped) = s.strip_suffix(SUFFIX_I2P_ALT) {
Ok(Self::Name {
name: stripped.to_owned(),
alt: true,
})
} else {
Err(I2pAddrParseError::NoSuffix(s.to_owned()))
}
}
}
impl Display for I2pAddr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.prefix(), self.suffix())
}
}
impl From<I2pAddr> for String {
fn from(addr: I2pAddr) -> Self { addr.to_string() }
}
impl TryFrom<String> for I2pAddr {
type Error = I2pAddrParseError;
fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(&s) }
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn roundtrip() {
for raw in [
"khpazz3f747z5zet72s6g3dccw53bfdqyhxt5da4sv7ouve5veuq.b32.i2p",
"udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p",
"i2p-projekt.i2p",
"shx5vqsw7usdaunyzr2qmes2fq37oumybpudrd4jjj4e4vk4uusa.b32.i2p",
"lhxgk47niirkle3zb35fyq2iyzgvpmzydjqbirzcjng3lookrmmxalzz.b32.i2p",
] {
assert_eq!(raw, I2pAddr::from_str(raw).unwrap().to_string());
let raw = raw.to_owned() + ".alt";
assert_eq!(raw, I2pAddr::from_str(&raw).unwrap().to_string());
}
}
#[test]
fn invalid() {
for raw in [
"khpazz3f747z5zet72s6g3dccw53bfdqyhxt5da4sv7ouve5veuq",
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.b32.i2p",
] {
assert!(I2pAddr::from_str(raw).is_err());
}
}
}