use alloc::string::String;
use alloc::vec::Vec;
use bip39::Language;
use crate::DeriveError;
const MIN_PREFIX_LEN: usize = 4;
pub fn expand(phrase: &str) -> Result<String, DeriveError> {
expand_in(Language::English, phrase)
}
pub fn expand_in(language: Language, phrase: &str) -> Result<String, DeriveError> {
let word_list = language.word_list();
let tokens: Vec<&str> = phrase.split_whitespace().collect();
let mut result = String::new();
for (i, token) in tokens.iter().enumerate() {
let word = resolve_token(word_list, token)?;
if i > 0 {
result.push(' ');
}
result.push_str(word);
}
Ok(result)
}
fn resolve_token<'a>(word_list: &'a [&'a str; 2048], token: &str) -> Result<&'a str, DeriveError> {
if let Ok(idx) = word_list.binary_search(&token) {
return word_list
.get(idx)
.copied()
.ok_or_else(|| DeriveError::UnknownPrefix(String::from(token)));
}
if token.len() < MIN_PREFIX_LEN {
return Err(DeriveError::PrefixTooShort {
prefix: String::from(token),
min_len: MIN_PREFIX_LEN,
});
}
let start = word_list.partition_point(|w| *w < token);
let matches: Vec<&str> = word_list
.get(start..)
.unwrap_or_default()
.iter()
.take_while(|w| w.starts_with(token))
.copied()
.collect();
match matches.len() {
0 => Err(DeriveError::UnknownPrefix(String::from(token))),
1 => matches
.first()
.copied()
.ok_or_else(|| DeriveError::UnknownPrefix(String::from(token))),
_ => Err(DeriveError::AmbiguousPrefix {
prefix: String::from(token),
candidates: matches.iter().map(|w| String::from(*w)).collect(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
const FULL_12: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
#[test]
fn full_words_unchanged() {
let result = expand(FULL_12).unwrap();
assert_eq!(result, FULL_12);
}
#[test]
fn four_letter_prefix_expansion() {
let abbreviated = "aban aban aban aban aban aban aban aban aban aban aban abou";
let result = expand(abbreviated).unwrap();
assert_eq!(result, FULL_12);
}
#[test]
fn mixed_full_and_abbreviated() {
let input =
"abandon aban abandon aban abandon aban abandon aban abandon aban abandon about";
let result = expand(input).unwrap();
assert_eq!(result, FULL_12);
}
#[test]
fn longer_prefix_works() {
let input =
"abando abando abando abando abando abando abando abando abando abando abando about";
let result = expand(input).unwrap();
assert_eq!(result, FULL_12);
}
#[test]
fn prefix_too_short_rejected() {
let result = expand("aba aba aba aba aba aba aba aba aba aba aba aba");
assert!(result.is_err());
assert!(
matches!(result, Err(DeriveError::PrefixTooShort { .. })),
"expected PrefixTooShort error"
);
}
#[test]
fn unknown_prefix_rejected() {
let result = expand("aban aban aban aban aban aban aban aban aban aban aban zzzz");
assert!(result.is_err());
assert!(
matches!(result, Err(DeriveError::UnknownPrefix(_))),
"expected UnknownPrefix error"
);
}
#[test]
fn ambiguous_prefix_rejected() {
let result = expand("aba");
assert!(result.is_err());
}
#[test]
fn preserves_word_count() {
let abbreviated = "aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban art";
let result = expand(abbreviated).unwrap();
assert_eq!(result.split_whitespace().count(), 24);
}
#[test]
fn different_words_expand_correctly() {
let input = "abil acti addr admi wall wris";
let result = expand(input).unwrap();
assert_eq!(result, "ability action address admit wall wrist");
}
#[test]
fn exact_short_words_accepted() {
let result = expand("zoo art ice");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "zoo art ice");
}
}