diff --git a/Cargo.toml b/Cargo.toml index fa51688..e61d3b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ publish = false [dependencies] anyhow = { version = "1.0.71", default-features = false, features = ["std"] } clap = { version = "4.3.11", default-features = false, features = ["derive", "std"] } +data-encoding = { version = "2.4.0", default-features = false, features = ["std"] } diff --git a/src/address.rs b/src/address.rs new file mode 100644 index 0000000..47b06a3 --- /dev/null +++ b/src/address.rs @@ -0,0 +1,249 @@ +use anyhow::{ensure, Error, Result}; +use data_encoding::{BASE32_NOPAD, BASE64}; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CodedAddress { + local_part: String, + sub_addr: String, + code: Vec, + domain: Option, +} + +impl FromStr for CodedAddress { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (local_part, domain) = split_local_part(s); + let parts: Vec<&str> = local_part.split(crate::DEFAULT_SEPARATOR).collect(); + ensure!(parts.len() == 3, "{s}: invalid number of parts"); + let local_part = parts[0].to_string(); + let sub_addr = parts[1].to_string(); + let code = BASE32_NOPAD.decode(parts[2].to_uppercase().as_bytes())?; + Ok(Self { + local_part, + sub_addr, + code, + domain, + }) + } +} + +#[derive(Clone, Debug)] +pub struct KeyedAddress { + local_part: String, + domain: Option, + key: Vec, +} + +impl KeyedAddress { + pub fn check_code(&self, addr: &CodedAddress) -> bool { + // TODO + false + } +} + +impl PartialEq for KeyedAddress { + fn eq(&self, other: &Self) -> bool { + self.local_part == other.local_part && self.domain == other.domain + } +} + +impl PartialEq for KeyedAddress { + fn eq(&self, other: &CodedAddress) -> bool { + self.local_part == other.local_part && self.domain == other.domain + } +} + +impl Eq for KeyedAddress {} + +impl Hash for KeyedAddress { + fn hash(&self, state: &mut H) { + self.local_part.hash(state); + self.domain.hash(state); + } +} + +impl FromStr for KeyedAddress { + type Err = Error; + + fn from_str(s: &str) -> Result { + let ksplit = s.rsplit_once(crate::KEY_SEPARATOR); + ensure!(ksplit.is_some(), "{s}: key separator not found"); + let (address, key_b64) = ksplit.unwrap(); + let (local_part, domain) = split_local_part(address); + let key = BASE64.decode(key_b64.as_bytes())?; + ensure!(!key.is_empty(), "{s}: key cannot be empty"); + Ok(Self { + local_part, + domain, + key, + }) + } +} + +fn split_local_part(s: &str) -> (String, Option) { + match s.rsplit_once('@') { + Some((local_part, domain)) => (local_part.to_string(), Some(domain.to_string())), + None => (s.to_string(), None), + } +} + +#[cfg(test)] +mod tests { + use super::{CodedAddress, KeyedAddress}; + use std::str::FromStr; + + #[test] + fn parse_valid_coded_addr_with_domain() { + let addr_str = "a+test+orsxg5a@example.org"; + let addr = CodedAddress::from_str(addr_str); + assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); + let addr = addr.unwrap(); + assert_eq!(addr.local_part, "a"); + assert_eq!(addr.sub_addr, "test"); + assert_eq!(addr.code, b"test"); + assert_eq!(addr.domain, Some("example.org".to_string())); + } + + #[test] + fn parse_valid_coded_addr_without_domain() { + let addr_str = "local.part+test+orsxg5a"; + let addr = CodedAddress::from_str(addr_str); + assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); + let addr = addr.unwrap(); + assert_eq!(addr.local_part, "local.part"); + assert_eq!(addr.domain, None); + } + + #[test] + fn parse_valid_keyed_addr_with_domain() { + let addr_str = "a@example.org:11voiefK5PgCX5F1TTcuoQ=="; + let addr = KeyedAddress::from_str(addr_str); + assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); + let addr = addr.unwrap(); + assert_eq!(addr.local_part, "a"); + assert_eq!(addr.domain, Some("example.org".to_string())); + assert_eq!( + addr.key, + vec![ + 0xd7, 0x5b, 0xe8, 0x89, 0xe7, 0xca, 0xe4, 0xf8, 0x02, 0x5f, 0x91, 0x75, 0x4d, 0x37, + 0x2e, 0xa1 + ] + ); + } + + #[test] + fn parse_valid_keyed_addr_without_domain() { + let addr_str = "local.part:3d74YQqk"; + let addr = KeyedAddress::from_str(addr_str); + assert!(addr.is_ok(), "unable to parse {addr_str}: {addr:?}"); + let addr = addr.unwrap(); + assert_eq!(addr.local_part, "local.part"); + assert_eq!(addr.domain, None); + assert_eq!(addr.key, vec![0xdd, 0xde, 0xf8, 0x61, 0x0a, 0xa4]); + } + + #[test] + fn keyed_addr_empty_address() { + let res = KeyedAddress::from_str(""); + assert!(res.is_err()); + } + + #[test] + fn keyed_addr_empty_base64() { + let res = KeyedAddress::from_str("a:"); + assert!(res.is_err()); + } + + #[test] + fn keyed_addr_invalid_base64() { + let res = KeyedAddress::from_str("a:uh2kv%j3"); + assert!(res.is_err()); + } + + #[test] + fn cmp_coded_addr_with_domain_eq() { + let addr_1 = CodedAddress::from_str("test+test+orsxg5a@example.org").unwrap(); + let addr_2 = CodedAddress::from_str("test+test+orsxg5a@example.org").unwrap(); + assert_eq!(addr_1, addr_2); + } + + #[test] + fn cmp_coded_addr_without_domain_eq() { + let addr_1 = CodedAddress::from_str("test+test+orsxg5a").unwrap(); + let addr_2 = CodedAddress::from_str("test+test+orsxg5a").unwrap(); + assert_eq!(addr_1, addr_2); + } + + #[test] + fn cmp_coded_addr_with_domain_ne() { + let addr_1 = CodedAddress::from_str("test+test+orsxg5a@example.org").unwrap(); + let addr_2 = CodedAddress::from_str("test2+test+orsxg5a@example.org").unwrap(); + assert_ne!(addr_1, addr_2); + } + + #[test] + fn cmp_coded_addr_without_domain_ne() { + let addr_1 = CodedAddress::from_str("test+test+orsxg5a").unwrap(); + let addr_2 = CodedAddress::from_str("test2+test+orsxg5a").unwrap(); + assert_ne!(addr_1, addr_2); + } + + #[test] + fn cmp_keyed_addr_with_domain_eq() { + let addr_1 = KeyedAddress::from_str("test@example.org:3d74YQqk").unwrap(); + let addr_2 = KeyedAddress::from_str("test@example.org:11voiefK5PgCX5F1TTcuoQ==").unwrap(); + assert_eq!(addr_1, addr_2); + } + + #[test] + fn cmp_keyed_addr_without_domain_eq() { + let addr_1 = KeyedAddress::from_str("test:3d74YQqk").unwrap(); + let addr_2 = KeyedAddress::from_str("test:11voiefK5PgCX5F1TTcuoQ==").unwrap(); + assert_eq!(addr_1, addr_2); + } + + #[test] + fn cmp_keyed_addr_with_domain_ne() { + let addr_1 = KeyedAddress::from_str("test@example.org:3d74YQqk").unwrap(); + let addr_2 = KeyedAddress::from_str("test2@example.org:3d74YQqk").unwrap(); + assert_ne!(addr_1, addr_2); + } + + #[test] + fn cmp_keyed_addr_without_domain_ne() { + let addr_1 = KeyedAddress::from_str("test:3d74YQqk").unwrap(); + let addr_2 = KeyedAddress::from_str("test2:3d74YQqk").unwrap(); + assert_ne!(addr_1, addr_2); + } + + #[test] + fn cmp_addr_types_with_domain_eq() { + let addr_1 = KeyedAddress::from_str("test@example.org:3d74YQqk").unwrap(); + let addr_2 = CodedAddress::from_str("test+test+orsxg5a@example.org").unwrap(); + assert_eq!(addr_1, addr_2); + } + + #[test] + fn cmp_addr_types_without_domain_eq() { + let addr_1 = KeyedAddress::from_str("test:3d74YQqk").unwrap(); + let addr_2 = CodedAddress::from_str("test+test+orsxg5a").unwrap(); + assert_eq!(addr_1, addr_2); + } + + #[test] + fn cmp_addr_types_with_domain_ne() { + let addr_1 = KeyedAddress::from_str("test@example.org:3d74YQqk").unwrap(); + let addr_2 = CodedAddress::from_str("test+test+orsxg5a@example.com").unwrap(); + assert_ne!(addr_1, addr_2); + } + + #[test] + fn cmp_addr_types_without_domain_ne() { + let addr_1 = KeyedAddress::from_str("test:3d74YQqk").unwrap(); + let addr_2 = CodedAddress::from_str("test2+test+orsxg5a").unwrap(); + assert_ne!(addr_1, addr_2); + } +} diff --git a/src/config.rs b/src/config.rs index 0779566..7de7177 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,11 @@ +use crate::address::KeyedAddress; use anyhow::Result; use clap::Parser; use std::collections::HashSet; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +use std::str::FromStr; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -17,10 +19,10 @@ pub struct Config { } impl Config { - pub fn addresses(&self) -> Result> { + pub fn addresses(&self) -> Result> { let mut addr_set = HashSet::new(); for addr in &self.address { - addr_set.insert(addr.to_string()); + addr_set.insert(KeyedAddress::from_str(addr)?); } if let Some(path) = &self.address_file { let f = File::open(path)?; @@ -29,7 +31,7 @@ impl Config { let line = line?; let addr = line.trim(); if !addr.is_empty() && !addr.starts_with(crate::COMMENT_CHAR) { - addr_set.insert(addr.to_string()); + addr_set.insert(KeyedAddress::from_str(addr)?); } } } diff --git a/src/main.rs b/src/main.rs index 54082b3..40b70d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use clap::Parser; +mod address; mod config; const COMMENT_CHAR: char = '#'; const DEFAULT_SEPARATOR: char = '+'; +const KEY_SEPARATOR: char = ':'; fn main() { let cfg = config::Config::parse();