use std::{fmt, path::PathBuf};
use serde::{Deserialize, Deserializer};
use crate::error::Error;
pub const CF_API_KEY: &str = "CF_API_KEY";
#[derive(Clone)]
pub struct Secret(String);
impl Secret {
pub fn expose(&self) -> &str {
&self.0
}
pub fn is_blank(&self) -> bool {
self.0.trim().is_empty()
}
pub fn fingerprint(&self) -> String {
const KEEP: usize = 4;
let chars: Vec<char> = self.0.chars().collect();
if chars.len() < KEEP * 3 {
return "<redacted>".to_owned();
}
let head: String = chars[..KEEP].iter().collect();
let tail: String = chars[chars.len() - KEEP..].iter().collect();
format!("{head}...{tail}")
}
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Secret(<redacted>)")
}
}
impl fmt::Display for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<redacted>")
}
}
impl<'de> Deserialize<'de> for Secret {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(Self)
}
}
pub fn load_dotenv() {
match dotenvy::dotenv() {
Ok(path) => log::debug!("loaded environment from \"{}\"", path.display()),
Err(err) if err.not_found() => {}
Err(err) => log::warn!("could not read .env: {err}"),
}
}
pub fn curseforge_api_key() -> Option<Secret> {
std::env::var(CF_API_KEY)
.ok()
.map(Secret)
.filter(|key| !key.is_blank())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_secret_redacts_itself_in_both_formats() {
let secret = Secret("hunter2".to_owned());
assert_eq!(format!("{secret}"), "<redacted>");
assert_eq!(format!("{secret:?}"), "Secret(<redacted>)");
assert_eq!(secret.expose(), "hunter2");
}
#[test]
fn a_fingerprint_shows_only_the_ends_of_a_long_value() {
let secret = Secret("$2a$10$abcdefghijklmnope345".to_owned());
assert_eq!(secret.fingerprint(), "$2a$...e345");
}
#[test]
fn a_secret_is_blank_only_when_there_is_nothing_in_it() {
assert!(Secret(String::new()).is_blank());
assert!(Secret(" \n ".to_owned()).is_blank());
assert!(!Secret("hunter2".to_owned()).is_blank());
}
#[test]
fn a_short_value_is_not_fingerprinted_at_all() {
assert_eq!(Secret("hunter2".to_owned()).fingerprint(), "<redacted>");
assert_eq!(Secret(String::new()).fingerprint(), "<redacted>");
}
}