Add coded and keyed addresses

This commit is contained in:
Rodolphe Bréard 2023-07-14 17:29:15 +02:00
parent b08b59e33f
commit 3e792358ed
4 changed files with 257 additions and 3 deletions

View file

@ -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"] }

249
src/address.rs Normal file
View file

@ -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<u8>,
domain: Option<String>,
}
impl FromStr for CodedAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
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<String>,
key: Vec<u8>,
}
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<CodedAddress> 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<H: Hasher>(&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<Self> {
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<String>) {
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);
}
}

View file

@ -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<HashSet<String>> {
pub fn addresses(&self) -> Result<HashSet<KeyedAddress>> {
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)?);
}
}
}

View file

@ -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();