1use unicode_normalization::UnicodeNormalization;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Language {
27 English,
28 Spanish,
29 French,
30 Italian,
31 Portuguese,
32 Japanese,
33 Korean,
34 ChineseSimplified,
35 ChineseTraditional,
36 Czech,
37}
38
39impl Language {
40 pub fn all() -> &'static [Language] {
43 &[
44 Language::English,
45 Language::Spanish,
46 Language::French,
47 Language::Italian,
48 Language::Portuguese,
49 Language::Japanese,
50 Language::Korean,
51 Language::ChineseSimplified,
52 Language::ChineseTraditional,
53 Language::Czech,
54 ]
55 }
56
57 fn to_bip39(self) -> bip39::Language {
60 match self {
61 Language::English => bip39::Language::English,
62 Language::Spanish => bip39::Language::Spanish,
63 Language::French => bip39::Language::French,
64 Language::Italian => bip39::Language::Italian,
65 Language::Portuguese => bip39::Language::Portuguese,
66 Language::Japanese => bip39::Language::Japanese,
67 Language::Korean => bip39::Language::Korean,
68 Language::ChineseSimplified => bip39::Language::SimplifiedChinese,
69 Language::ChineseTraditional => bip39::Language::TraditionalChinese,
70 Language::Czech => bip39::Language::Czech,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Report {
79 pub language: Option<Language>,
81 pub word_count: usize,
83 pub entropy_bits: Option<usize>,
85 pub normalized: String,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct CheckError {
93 pub kind: ErrorKind,
94 pub details: Option<String>,
95}
96
97impl std::fmt::Display for CheckError {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 use ErrorKind::*;
100 match &self.kind {
101 Empty => write!(f, "mnemonic vazio")?,
103 InvalidWordCount { got } => write!(f, "contagem de palavras inválida: {}", got)?,
104 UnknownOrMixedWords => write!(f, "palavras desconhecidas ou mistura de idiomas")?,
105 BadChecksum => write!(f, "checksum inválido")?,
106 UnicodeSuspicious => write!(f, "unicode suspeito após normalização")?,
107 }
108 if let Some(d) = &self.details {
109 write!(f, " ({})", d)?;
110 }
111 Ok(())
112 }
113}
114
115impl std::error::Error for CheckError {}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum ErrorKind {
121 Empty,
122 InvalidWordCount { got: usize },
123 UnknownOrMixedWords,
124 BadChecksum,
125 UnicodeSuspicious,
126}
127
128pub fn validate(phrase: &str) -> Result<Report, CheckError> {
140 let normalized = phrase.nfkd().collect::<String>().trim().to_string();
142 if normalized.is_empty() {
143 return Err(CheckError { kind: ErrorKind::Empty, details: None });
144 }
145
146 let words: Vec<&str> = normalized.split_whitespace().collect();
148 let wc = words.len();
149 const ALLOWED: [usize; 5] = [12, 15, 18, 21, 24];
150 if !ALLOWED.contains(&wc) {
151 return Err(CheckError {
152 kind: ErrorKind::InvalidWordCount { got: wc },
153 details: Some("válidos: 12, 15, 18, 21, 24".into()),
154 });
155 }
156
157 if normalized.chars().any(|c| {
159 (c as u32) < 0x20 || (0x202A..=0x202E).contains(&(c as u32))
160 }) {
161 return Err(CheckError { kind: ErrorKind::UnicodeSuspicious, details: None });
162 }
163
164 let mut saw_checksum_err = false;
166 for lang in Language::all() {
167 match bip39::Mnemonic::parse_in_normalized(lang.to_bip39(), &normalized) {
168 Ok(m) => {
169 let entropy_bits = m.to_entropy().len() * 8;
170 return Ok(Report {
171 language: Some(*lang),
172 word_count: wc,
173 entropy_bits: Some(entropy_bits),
174 normalized,
175 });
176 }
177 Err(e) => {
178 if e.to_string().to_lowercase().contains("checksum") {
181 saw_checksum_err = true;
182 }
183 }
184 }
185 }
186
187 if saw_checksum_err {
188 Err(CheckError { kind: ErrorKind::BadChecksum, details: None })
189 } else {
190 Err(CheckError { kind: ErrorKind::UnknownOrMixedWords, details: None })
191 }
192}
193
194pub fn is_valid(phrase: &str) -> bool {
197 validate(phrase).is_ok()
198}