use unicode_normalization::UnicodeNormalization;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Language {
English,
Spanish,
French,
Italian,
Portuguese,
Japanese,
Korean,
ChineseSimplified,
ChineseTraditional,
Czech,
}
impl Language {
pub fn all() -> &'static [Language] {
&[
Language::English,
Language::Spanish,
Language::French,
Language::Italian,
Language::Portuguese,
Language::Japanese,
Language::Korean,
Language::ChineseSimplified,
Language::ChineseTraditional,
Language::Czech,
]
}
fn to_bip39(self) -> bip39::Language {
match self {
Language::English => bip39::Language::English,
Language::Spanish => bip39::Language::Spanish,
Language::French => bip39::Language::French,
Language::Italian => bip39::Language::Italian,
Language::Portuguese => bip39::Language::Portuguese,
Language::Japanese => bip39::Language::Japanese,
Language::Korean => bip39::Language::Korean,
Language::ChineseSimplified => bip39::Language::SimplifiedChinese,
Language::ChineseTraditional => bip39::Language::TraditionalChinese,
Language::Czech => bip39::Language::Czech,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Report {
pub language: Option<Language>,
pub word_count: usize,
pub entropy_bits: Option<usize>,
pub normalized: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckError {
pub kind: ErrorKind,
pub details: Option<String>,
}
impl std::fmt::Display for CheckError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ErrorKind::*;
match &self.kind {
Empty => write!(f, "mnemonic vazio")?,
InvalidWordCount { got } => write!(f, "contagem de palavras inválida: {}", got)?,
UnknownOrMixedWords => write!(f, "palavras desconhecidas ou mistura de idiomas")?,
BadChecksum => write!(f, "checksum inválido")?,
UnicodeSuspicious => write!(f, "unicode suspeito após normalização")?,
}
if let Some(d) = &self.details {
write!(f, " ({})", d)?;
}
Ok(())
}
}
impl std::error::Error for CheckError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
Empty,
InvalidWordCount { got: usize },
UnknownOrMixedWords,
BadChecksum,
UnicodeSuspicious,
}
pub fn validate(phrase: &str) -> Result<Report, CheckError> {
let normalized = phrase.nfkd().collect::<String>().trim().to_string();
if normalized.is_empty() {
return Err(CheckError { kind: ErrorKind::Empty, details: None });
}
let words: Vec<&str> = normalized.split_whitespace().collect();
let wc = words.len();
const ALLOWED: [usize; 5] = [12, 15, 18, 21, 24];
if !ALLOWED.contains(&wc) {
return Err(CheckError {
kind: ErrorKind::InvalidWordCount { got: wc },
details: Some("válidos: 12, 15, 18, 21, 24".into()),
});
}
if normalized.chars().any(|c| {
(c as u32) < 0x20 || (0x202A..=0x202E).contains(&(c as u32))
}) {
return Err(CheckError { kind: ErrorKind::UnicodeSuspicious, details: None });
}
let mut saw_checksum_err = false;
for lang in Language::all() {
match bip39::Mnemonic::parse_in_normalized(lang.to_bip39(), &normalized) {
Ok(m) => {
let entropy_bits = m.to_entropy().len() * 8;
return Ok(Report {
language: Some(*lang),
word_count: wc,
entropy_bits: Some(entropy_bits),
normalized,
});
}
Err(e) => {
if e.to_string().to_lowercase().contains("checksum") {
saw_checksum_err = true;
}
}
}
}
if saw_checksum_err {
Err(CheckError { kind: ErrorKind::BadChecksum, details: None })
} else {
Err(CheckError { kind: ErrorKind::UnknownOrMixedWords, details: None })
}
}
pub fn is_valid(phrase: &str) -> bool {
validate(phrase).is_ok()
}