use crate::{Compatible, DefaultMeta, Text, quex};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Hashed(String);
impl Hashed {
pub async fn make(plain: impl Into<String>) -> Result<Self, HashError> {
Self::make_with_cost(plain, bcrypt::DEFAULT_COST).await
}
pub async fn make_with_cost(plain: impl Into<String>, cost: u32) -> Result<Self, HashError> {
let plain = plain.into();
let hashed = tokio::task::spawn_blocking(move || bcrypt::non_truncating_hash(plain, cost))
.await
.map_err(HashError::Join)?
.map_err(HashError::Bcrypt)?;
Ok(Self(hashed))
}
pub async fn verify(&self, plain: impl AsRef<str>) -> Result<bool, HashError> {
let plain = plain.as_ref().to_owned();
let hash = self.0.clone();
tokio::task::spawn_blocking(move || bcrypt::verify(plain, &hash))
.await
.map_err(HashError::Join)?
.map_err(HashError::Bcrypt)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl DefaultMeta for Hashed {
type Meta = Text;
}
impl Compatible<Text> for Hashed {}
impl quex::Encode for Hashed {
fn encode(&self, out: quex::Encoder<'_>) {
self.0.encode(out);
}
}
impl quex::Decode for Hashed {
fn decode(value: &mut quex::Decoder<'_>) -> quex::Result<Self> {
Ok(Self(<String as quex::Decode>::decode(value)?))
}
}
#[derive(Debug)]
pub enum HashError {
Bcrypt(bcrypt::BcryptError),
Join(tokio::task::JoinError),
}
impl std::fmt::Display for HashError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bcrypt(error) => error.fmt(f),
Self::Join(error) => error.fmt(f),
}
}
}
impl std::error::Error for HashError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Bcrypt(error) => Some(error),
Self::Join(error) => Some(error),
}
}
}
impl From<bcrypt::BcryptError> for HashError {
fn from(value: bcrypt::BcryptError) -> Self {
Self::Bcrypt(value)
}
}
impl From<tokio::task::JoinError> for HashError {
fn from(value: tokio::task::JoinError) -> Self {
Self::Join(value)
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::HashError;
#[test]
fn hash_error_preserves_join_source() {
let runtime = tokio::runtime::Builder::new_current_thread()
.build()
.expect("runtime should build");
let error = runtime.block_on(async {
let handle = tokio::spawn(async {
panic!("boom");
});
HashError::from(handle.await.expect_err("join should fail"))
});
let source = error.source().expect("hash error should expose source");
assert!(!source.to_string().is_empty());
}
}