248 lines
8.5 KiB
Rust
248 lines
8.5 KiB
Rust
|
|
use std::fmt;
|
|
|
|
mod english;
|
|
#[cfg(feature = "chinese-simplified")]
|
|
mod chinese_simplified;
|
|
#[cfg(feature = "chinese-traditional")]
|
|
mod chinese_traditional;
|
|
#[cfg(feature = "czech")]
|
|
mod czech;
|
|
#[cfg(feature = "french")]
|
|
mod french;
|
|
#[cfg(feature = "italian")]
|
|
mod italian;
|
|
#[cfg(feature = "japanese")]
|
|
mod japanese;
|
|
#[cfg(feature = "korean")]
|
|
mod korean;
|
|
#[cfg(feature = "spanish")]
|
|
mod spanish;
|
|
|
|
#[cfg(not(feature = "low-memory"))]
|
|
mod lazy {
|
|
use std::cell::Cell;
|
|
use std::collections::HashMap;
|
|
use std::sync::Once;
|
|
|
|
/// Type used to load a word map in a lazy fashion.
|
|
pub(crate) struct LazyMap(Cell<Option<HashMap<&'static str, u16>>>, Once);
|
|
|
|
impl LazyMap {
|
|
#[allow(deprecated)]
|
|
const INIT: Self = LazyMap(Cell::new(None), ::std::sync::ONCE_INIT);
|
|
|
|
#[inline(always)]
|
|
pub fn get(&'static self, list: &'static [&'static str]) -> &HashMap<&'static str, u16> {
|
|
self.1.call_once(|| {
|
|
let mut map = HashMap::new();
|
|
for (idx, word) in list.iter().enumerate() {
|
|
map.insert(*word, idx as u16);
|
|
}
|
|
self.0.set(Some(map));
|
|
});
|
|
|
|
// `self.0` is guaranteed to be `Some` by this point
|
|
// The `Once` will catch and propagate panics
|
|
unsafe {
|
|
match *self.0.as_ptr() {
|
|
Some(ref x) => x,
|
|
None => panic!(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This marker impl is required for the Cell to work.
|
|
// The LazyMap is an implementation identical to lazy_static's.
|
|
// We assume lazy_static's exact same usage is considered safe.
|
|
#[cfg(not(feature = "low-memory"))]
|
|
unsafe impl Sync for LazyMap {}
|
|
|
|
pub(crate) static LAZY_MAP_ENGLISH: LazyMap = LazyMap::INIT;
|
|
#[cfg(feature = "chinese-simplified")]
|
|
pub(crate) static LAZY_MAP_CHINESE_SIMPLIFIED: LazyMap = LazyMap::INIT;
|
|
#[cfg(feature = "chinese-traditional")]
|
|
pub(crate) static LAZY_MAP_CHINESE_TRADITIONAL: LazyMap = LazyMap::INIT;
|
|
#[cfg(feature = "czech")]
|
|
pub(crate) static LAZY_MAP_CZECH: LazyMap = LazyMap::INIT;
|
|
#[cfg(feature = "french")]
|
|
pub(crate) static LAZY_MAP_FRENCH: LazyMap = LazyMap::INIT;
|
|
#[cfg(feature = "italian")]
|
|
pub(crate) static LAZY_MAP_ITALIAN: LazyMap = LazyMap::INIT;
|
|
#[cfg(feature = "japanese")]
|
|
pub(crate) static LAZY_MAP_JAPANESE: LazyMap = LazyMap::INIT;
|
|
#[cfg(feature = "korean")]
|
|
pub(crate) static LAZY_MAP_KOREAN: LazyMap = LazyMap::INIT;
|
|
#[cfg(feature = "spanish")]
|
|
pub(crate) static LAZY_MAP_SPANISH: LazyMap = LazyMap::INIT;
|
|
}
|
|
|
|
/// Language to be used for the mnemonic phrase.
|
|
///
|
|
/// The English language is always available, other languages are enabled using
|
|
/// the compilation features.
|
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub enum Language {
|
|
/// The English language.
|
|
English,
|
|
#[cfg(feature = "chinese-simplified")]
|
|
/// The Simplified Chinese language.
|
|
SimplifiedChinese,
|
|
#[cfg(feature = "chinese-traditional")]
|
|
/// The Traditional Chinese language.
|
|
TraditionalChinese,
|
|
#[cfg(feature = "czech")]
|
|
/// The Czech language.
|
|
Czech,
|
|
#[cfg(feature = "french")]
|
|
/// The French language.
|
|
French,
|
|
#[cfg(feature = "italian")]
|
|
/// The Italian language.
|
|
Italian,
|
|
#[cfg(feature = "japanese")]
|
|
/// The Japanese language.
|
|
Japanese,
|
|
#[cfg(feature = "korean")]
|
|
/// The Korean language.
|
|
Korean,
|
|
#[cfg(feature = "spanish")]
|
|
/// The Spanish language.
|
|
Spanish,
|
|
}
|
|
|
|
impl Language {
|
|
/// Get words from the wordlist that start with the given prefix.
|
|
pub fn words_by_prefix(self, prefix: &str) -> &[&'static str] {
|
|
let first = match self.word_list().iter().position(|w| w.starts_with(prefix)) {
|
|
Some(i) => i,
|
|
None => return &[],
|
|
};
|
|
let count = self.word_list()[first..].iter().take_while(|w| w.starts_with(prefix)).count();
|
|
&self.word_list()[first .. first + count]
|
|
}
|
|
|
|
/// The word list for this language.
|
|
#[inline]
|
|
pub(crate) fn word_list(self) -> &'static [&'static str; 2048] {
|
|
match self {
|
|
Language::English => &english::WORDS,
|
|
#[cfg(feature = "chinese-simplified")]
|
|
Language::SimplifiedChinese => &chinese_simplified::WORDS,
|
|
#[cfg(feature = "chinese-traditional")]
|
|
Language::TraditionalChinese => &chinese_traditional::WORDS,
|
|
#[cfg(feature = "czech")]
|
|
Language::Czech => &czech::WORDS,
|
|
#[cfg(feature = "french")]
|
|
Language::French => &french::WORDS,
|
|
#[cfg(feature = "italian")]
|
|
Language::Italian => &italian::WORDS,
|
|
#[cfg(feature = "japanese")]
|
|
Language::Japanese => &japanese::WORDS,
|
|
#[cfg(feature = "korean")]
|
|
Language::Korean => &korean::WORDS,
|
|
#[cfg(feature = "spanish")]
|
|
Language::Spanish => &spanish::WORDS,
|
|
}
|
|
}
|
|
|
|
/// The word map that maps words to the index in the word list for this language.
|
|
#[cfg(not(feature = "low-memory"))]
|
|
pub(crate) fn word_map(self) -> &'static ::std::collections::HashMap<&'static str, u16> {
|
|
match self {
|
|
Language::English => lazy::LAZY_MAP_ENGLISH.get(self.word_list()),
|
|
#[cfg(feature = "chinese-simplified")]
|
|
Language::SimplifiedChinese => lazy::LAZY_MAP_CHINESE_SIMPLIFIED.get(self.word_list()),
|
|
#[cfg(feature = "chinese-traditional")]
|
|
Language::TraditionalChinese => lazy::LAZY_MAP_CHINESE_TRADITIONAL.get(self.word_list()),
|
|
#[cfg(feature = "czech")]
|
|
Language::Czech => lazy::LAZY_MAP_CZECH.get(self.word_list()),
|
|
#[cfg(feature = "french")]
|
|
Language::French => lazy::LAZY_MAP_FRENCH.get(self.word_list()),
|
|
#[cfg(feature = "italian")]
|
|
Language::Italian => lazy::LAZY_MAP_ITALIAN.get(self.word_list()),
|
|
#[cfg(feature = "japanese")]
|
|
Language::Japanese => lazy::LAZY_MAP_JAPANESE.get(self.word_list()),
|
|
#[cfg(feature = "korean")]
|
|
Language::Korean => lazy::LAZY_MAP_KOREAN.get(self.word_list()),
|
|
#[cfg(feature = "spanish")]
|
|
Language::Spanish => lazy::LAZY_MAP_SPANISH.get(self.word_list()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Language {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
fmt::Debug::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[cfg(all(
|
|
feature = "chinese-simplified", feature = "chinese-traditional", feature = "czech",
|
|
feature = "french", feature = "italian", feature = "japanese", feature = "korean",
|
|
feature = "spanish"
|
|
))]
|
|
#[test]
|
|
fn validate_wordlist_checksums() {
|
|
//! In this test, we ensure that the wordlists are identical.
|
|
//!
|
|
//! They are as follows in the bips repository:
|
|
//! 5c5942792bd8340cb8b27cd592f1015edf56a8c5b26276ee18a482428e7c5726 chinese_simplified.txt
|
|
//! 417b26b3d8500a4ae3d59717d7011952db6fc2fb84b807f3f94ac734e89c1b5f chinese_traditional.txt
|
|
//! 7e80e161c3e93d9554c2efb78d4e3cebf8fc727e9c52e03b83b94406bdcc95fc czech.txt
|
|
//! 2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda english.txt
|
|
//! ebc3959ab7801a1df6bac4fa7d970652f1df76b683cd2f4003c941c63d517e59 french.txt
|
|
//! d392c49fdb700a24cd1fceb237c1f65dcc128f6b34a8aacb58b59384b5c648c2 italian.txt
|
|
//! 2eed0aef492291e061633d7ad8117f1a2b03eb80a29d0e4e3117ac2528d05ffd japanese.txt
|
|
//! 9e95f86c167de88f450f0aaf89e87f6624a57f973c67b516e338e8e8b8897f60 korean.txt
|
|
//! 46846a5a0139d1e3cb77293e521c2865f7bcdb82c44e8d0a06a2cd0ecba48c0b spanish.txt
|
|
|
|
use std::io::Write;
|
|
use bitcoin_hashes::{sha256, Hash};
|
|
|
|
let checksums = [
|
|
("5c5942792bd8340cb8b27cd592f1015edf56a8c5b26276ee18a482428e7c5726", Language::SimplifiedChinese),
|
|
("417b26b3d8500a4ae3d59717d7011952db6fc2fb84b807f3f94ac734e89c1b5f", Language::TraditionalChinese),
|
|
("7e80e161c3e93d9554c2efb78d4e3cebf8fc727e9c52e03b83b94406bdcc95fc", Language::Czech),
|
|
("2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda", Language::English),
|
|
("ebc3959ab7801a1df6bac4fa7d970652f1df76b683cd2f4003c941c63d517e59", Language::French),
|
|
("d392c49fdb700a24cd1fceb237c1f65dcc128f6b34a8aacb58b59384b5c648c2", Language::Italian),
|
|
("2eed0aef492291e061633d7ad8117f1a2b03eb80a29d0e4e3117ac2528d05ffd", Language::Japanese),
|
|
("9e95f86c167de88f450f0aaf89e87f6624a57f973c67b516e338e8e8b8897f60", Language::Korean),
|
|
("46846a5a0139d1e3cb77293e521c2865f7bcdb82c44e8d0a06a2cd0ecba48c0b", Language::Spanish),
|
|
];
|
|
|
|
for &(sum, lang) in &checksums {
|
|
let mut digest = sha256::Hash::engine();
|
|
for (_idx, word) in lang.word_list().iter().enumerate() {
|
|
assert!(::unicode_normalization::is_nfkd(&word));
|
|
write!(&mut digest, "{}\n", word).unwrap();
|
|
#[cfg(not(feature = "low-memory"))]
|
|
assert_eq!(_idx, lang.word_map()[word] as usize);
|
|
}
|
|
assert_eq!(&sha256::Hash::from_engine(digest).to_string(), sum,
|
|
"word list for language {} failed checksum check", lang,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn words_by_prefix() {
|
|
let lang = Language::English;
|
|
|
|
let res = lang.words_by_prefix("woo");
|
|
assert_eq!(res, ["wood","wool"]);
|
|
|
|
let res = lang.words_by_prefix("");
|
|
assert_eq!(res.len(), 2048);
|
|
|
|
let res = lang.words_by_prefix("woof");
|
|
assert!(res.is_empty());
|
|
}
|
|
}
|