use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use regex::Regex;
use super::error::Error;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AuthorId {
key: String,
is_digest: bool,
}
impl AuthorId {
#[must_use]
pub fn new(name: &[u8], email: &[u8]) -> Self {
let email_key = String::from_utf8_lossy(email).trim().to_lowercase();
let key = if email_key.is_empty() {
String::from_utf8_lossy(name).trim().to_lowercase()
} else {
email_key
};
Self {
key,
is_digest: false,
}
}
#[must_use]
pub fn has_identity(&self) -> bool {
!self.key.is_empty()
}
#[must_use]
pub fn from_digest(digest: String) -> Self {
Self {
key: digest,
is_digest: true,
}
}
#[must_use]
pub fn hashed(&self) -> String {
if self.is_digest {
return self.key.clone();
}
let mut hasher = Sha256::new();
hasher.update(self.key.as_bytes());
to_hex(&hasher.finalize())
}
#[must_use]
pub fn emit_hashed(&self, key: Option<&AuthorHashKey>) -> String {
let base = self.hashed();
match key {
None => base,
Some(key) => key.apply(&base),
}
}
}
#[derive(Clone)]
pub struct AuthorHashKey {
key: Vec<u8>,
}
impl AuthorHashKey {
pub fn new(key: Vec<u8>) -> Result<Self, Error> {
if key.is_empty() {
return Err(Error::InvalidAuthorHashKey("the key is empty".to_owned()));
}
Ok(Self { key })
}
#[must_use]
pub(crate) fn apply(&self, digest_hex: &str) -> String {
let mut mac =
Hmac::<Sha256>::new_from_slice(&self.key).expect("HMAC accepts a key of any length");
mac.update(digest_hex.as_bytes());
to_hex(&mac.finalize().into_bytes())
}
}
impl std::fmt::Debug for AuthorHashKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthorHashKey").finish_non_exhaustive()
}
}
fn to_hex(bytes: &[u8]) -> String {
let mut hex = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(hex, "{byte:02x}");
}
hex
}
#[derive(Clone, Debug)]
pub struct BotFilter {
pattern: Regex,
}
impl BotFilter {
pub fn new(pattern: &str) -> Result<Self, Error> {
let pattern = Regex::new(pattern).map_err(|e| Error::InvalidBotPattern(e.to_string()))?;
Ok(Self { pattern })
}
#[must_use]
pub fn is_bot(&self, name: &[u8], email: &[u8]) -> bool {
let name = String::from_utf8_lossy(name);
let email = String::from_utf8_lossy(email);
self.pattern.is_match(&name) || self.pattern.is_match(&email)
}
}
#[cfg(test)]
#[path = "identity_tests.rs"]
mod tests;