ultimate 0.1.0

Rust libraries
Documentation
use std::sync::OnceLock;

use argon2::password_hash::SaltString;
use argon2::{password_hash, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use regex::Regex;
use tracing::{error, trace};

use super::{Error, Result};

const CUR_PWD_VERSION: u16 = 1;

pub async fn generate_pwd(password: &str) -> Result<String> {
  let hash = try_to_hash(password).await?;
  Ok(format!("#{}#{}", CUR_PWD_VERSION, hash))
}

pub async fn verify_pwd(password: &str, hashed_pwd: &str) -> Result<u16> {
  let (version, hash) = split_pwd_version(hashed_pwd);
  if verify(password.as_bytes(), hash).await? {
    Ok(version)
  } else {
    Err(Error::InvalidPassword)
  }
}

pub(crate) async fn try_to_hash(password: &str) -> Result<String> {
  let salt = SaltString::generate(rand::thread_rng());
  let hash = Argon2::default()
    .hash_password(password.as_bytes(), &salt)
    .map_err(|e| {
      error!("Failed to hash password: {}", e.to_string());
      Error::FailedToHashPassword
    })?
    .to_string();
  Ok(hash)
}

pub(crate) async fn verify(password: &[u8], hash: &str) -> Result<bool> {
  let hash = PasswordHash::new(hash).map_err(|e| {
    error!("BUG: password hash invalid: {}", e.to_string());
    Error::InvalidFormat
  })?;

  let res = Argon2::default().verify_password(password, &hash);

  match res {
    Ok(()) => Ok(true),
    Err(password_hash::Error::Password) => Ok(false),
    Err(e) => {
      error!("Failed to verify password: {}", e.to_string());
      Err(Error::FailedToVerifyPassword)
    }
  }
}

static SPLIT_PWD_RE: OnceLock<Regex> = OnceLock::new();

fn split_pwd_version(pwd: &str) -> (u16, &str) {
  let re = SPLIT_PWD_RE.get_or_init(|| Regex::new(r"^#(?<version>\d+)#").unwrap());
  if let Some(caps) = re.captures(pwd) {
    trace!("The version of pwd is {:?}", caps);
    let version = caps.name("version").unwrap();
    trace!(
      "start:{}, end:{}, len:{}, range:{:?}, str:{}",
      version.start(),
      version.end(),
      version.len(),
      version.range(),
      version.as_str()
    );

    let hash = &pwd[version.end() + 1..];
    (version.as_str().parse().unwrap(), hash)
  } else {
    (0, pwd)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[tokio::test]
  async fn test_pwd() -> Result<()> {
    let password = "Lightshadow.2024";
    let pwd = generate_pwd(password).await?;
    println!("The pwd is {pwd}");

    assert!(pwd.starts_with("#1#"));

    let version = verify_pwd(password, &pwd).await?;
    assert_eq!(version, 1);

    Ok(())
  }
}