use base_d::{DictionaryRegistry, decode, encode};
use std::fs;
use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::time::Duration;
use crossterm::event::{Event, KeyCode, KeyEvent, poll, read};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use super::config::{
BuiltDictionary, create_any_dictionary, create_dictionary, get_compression_level,
load_xxhash_config,
};
pub enum SwitchInterval {
Time(Duration),
PerLine,
}
pub enum SwitchMode {
Static,
Cycle(SwitchInterval),
Random(SwitchInterval),
}
pub fn parse_interval(s: &str) -> Result<SwitchInterval, Box<dyn std::error::Error>> {
if s == "line" {
return Ok(SwitchInterval::PerLine);
}
let s = s.trim();
let split_pos = s
.chars()
.position(|c| !c.is_ascii_digit())
.ok_or("Invalid duration format")?;
let (num_str, unit) = s.split_at(split_pos);
let value: u64 = num_str.parse()?;
let duration = match unit {
"ms" => Duration::from_millis(value),
"s" => Duration::from_secs(value),
"m" => Duration::from_secs(value * 60),
_ => return Err(format!("Unknown duration unit: {}", unit).into()),
};
Ok(SwitchInterval::Time(duration))
}
pub fn select_random_dictionary(
config: &DictionaryRegistry,
print_message: bool,
) -> Result<String, Box<dyn std::error::Error>> {
use rand::prelude::IndexedRandom;
let mut rng = rand::rng();
let dict_names: Vec<&String> = config
.dictionaries
.iter()
.filter(|(_, cfg)| cfg.common)
.map(|(name, _)| name)
.collect();
let random_dict = dict_names
.choose(&mut rng)
.ok_or("No common dictionaries available")?;
if print_message {
eprintln!(
"Note: Using randomly selected dictionary '{}' (use --encode={} to fix)",
random_dict, random_dict
);
}
Ok(random_dict.to_string())
}
#[allow(dead_code)]
pub const HASH_ALGORITHMS: &[&str] = &[
"md5", "sha256", "sha512", "blake3", "ascon", "k12", "xxh64", "xxh3",
];
pub const COMPRESS_ALGORITHMS: &[&str] = &["gzip", "zstd", "brotli", "lz4"];
#[allow(dead_code)]
pub fn select_random_hash(quiet: bool) -> &'static str {
use rand::prelude::IndexedRandom;
let selected = HASH_ALGORITHMS.choose(&mut rand::rng()).unwrap();
if !quiet {
eprintln!(
"Note: Using randomly selected hash '{}' (use --hash={} to fix)",
selected, selected
);
}
selected
}
pub fn select_random_compress(quiet: bool) -> &'static str {
use rand::prelude::IndexedRandom;
let selected = COMPRESS_ALGORITHMS.choose(&mut rand::rng()).unwrap();
if !quiet {
eprintln!(
"Note: Using randomly selected compression '{}' (use --compress={} to fix)",
selected, selected
);
}
selected
}
fn generate_word_line<R: rand::Rng>(
rng: &mut R,
word_dict: &base_d::WordDictionary,
term_width: usize,
) -> String {
use rand::prelude::IndexedRandom;
let words: Vec<&str> = word_dict.words().collect();
if words.is_empty() {
return String::new();
}
let delimiter = word_dict.delimiter();
let mut line = String::new();
let mut current_len = 0;
loop {
let word = words.choose(rng).unwrap();
let word_len = word.chars().count();
let delimiter_len = if line.is_empty() {
0
} else {
delimiter.chars().count()
};
if current_len + delimiter_len + word_len > term_width {
if line.is_empty() && term_width > 0 {
return word.chars().take(term_width).collect();
}
break;
}
if !line.is_empty() {
line.push_str(delimiter);
current_len += delimiter_len;
}
line.push_str(word);
current_len += word_len;
}
line
}
fn generate_alternating_word_line<R: rand::Rng>(
rng: &mut R,
alt_dict: &base_d::AlternatingWordDictionary,
term_width: usize,
) -> String {
use rand::prelude::IndexedRandom;
let num_dicts = alt_dict.num_dicts();
if num_dicts == 0 {
return String::new();
}
let delimiter = alt_dict.delimiter();
let mut line = String::new();
let mut current_len = 0;
let mut word_position = 0;
loop {
let current_dict = alt_dict.dict_at(word_position);
let words: Vec<&str> = current_dict.words().collect();
if words.is_empty() {
break;
}
let word = words.choose(rng).unwrap();
let word_len = word.chars().count();
let delimiter_len = if line.is_empty() {
0
} else {
delimiter.chars().count()
};
if current_len + delimiter_len + word_len > term_width {
if line.is_empty() && term_width > 0 {
return word.chars().take(term_width).collect();
}
break;
}
if !line.is_empty() {
line.push_str(delimiter);
current_len += delimiter_len;
}
line.push_str(word);
current_len += word_len;
word_position += 1;
}
line
}
pub fn matrix_mode(
config: &DictionaryRegistry,
initial_dictionary: &str,
switch_mode: SwitchMode,
no_color: bool,
quiet: bool,
superman: bool,
) -> Result<(), Box<dyn std::error::Error>> {
use std::thread;
use std::time::Instant;
enable_raw_mode()?;
if !no_color {
print!("\x1b[2J\x1b[H"); print!("\x1b[32m"); }
let messages = [
"Wake up, Neo...",
"The Matrix has you...",
"Follow the white rabbit.",
"Knock, knock, Neo.",
];
'intro_loop: for message in &messages {
for ch in message.chars() {
print!("{}", ch);
io::stdout().flush()?;
if poll(Duration::from_millis(100))? {
if let Event::Key(KeyEvent { code, .. }) = read()?
&& matches!(code, KeyCode::Esc | KeyCode::Char(' ') | KeyCode::Enter)
{
if !no_color {
print!("\r\x1b[K");
} else {
print!("\r");
}
break 'intro_loop;
}
} else {
thread::sleep(Duration::from_millis(100));
}
}
thread::sleep(Duration::from_millis(800));
if !no_color {
print!("\r\x1b[K");
} else {
print!("\r");
}
io::stdout().flush()?;
thread::sleep(Duration::from_millis(200));
}
thread::sleep(Duration::from_millis(500));
let mut current_dictionary_name = initial_dictionary.to_string();
let sorted_dicts: Vec<String> = if matches!(switch_mode, SwitchMode::Cycle(_)) {
let mut names: Vec<String> = config.dictionaries.keys().cloned().collect();
names.sort();
names
} else {
Vec::new()
};
let mut cycle_index = sorted_dicts
.iter()
.position(|n| n == ¤t_dictionary_name)
.unwrap_or(0);
let mut last_switch = Instant::now();
let mut rng = rand::rng();
let dict_names: Vec<String> = {
let mut names: Vec<_> = config.dictionaries.keys().cloned().collect();
names.sort();
names
};
let mut current_index = dict_names
.iter()
.position(|n| n == ¤t_dictionary_name)
.unwrap_or(0);
if !quiet {
if !no_color {
eprint!("\x1b[32mDictionary: {}\x1b[0m\r\n", current_dictionary_name);
} else {
eprint!("Dictionary: {}\r\n", current_dictionary_name);
}
}
loop {
let built_dictionary = create_any_dictionary(config, ¤t_dictionary_name)?;
let should_switch = match &switch_mode {
SwitchMode::Cycle(SwitchInterval::Time(d))
| SwitchMode::Random(SwitchInterval::Time(d)) => last_switch.elapsed() >= *d,
_ => false,
};
if should_switch {
match &switch_mode {
SwitchMode::Cycle(_) => {
cycle_index = (cycle_index + 1) % sorted_dicts.len();
current_dictionary_name = sorted_dicts[cycle_index].clone();
}
SwitchMode::Random(_) => {
current_dictionary_name = select_random_dictionary(config, false)?;
}
_ => {}
}
if !quiet {
if !no_color {
eprint!("\x1b[32mDictionary: {}\x1b[0m\r\n", current_dictionary_name);
} else {
eprint!("Dictionary: {}\r\n", current_dictionary_name);
}
}
last_switch = Instant::now();
continue; }
let switch_per_line = matches!(
&switch_mode,
SwitchMode::Cycle(SwitchInterval::PerLine)
| SwitchMode::Random(SwitchInterval::PerLine)
);
if switch_per_line {
match &switch_mode {
SwitchMode::Cycle(SwitchInterval::PerLine) => {
cycle_index = (cycle_index + 1) % sorted_dicts.len();
current_dictionary_name = sorted_dicts[cycle_index].clone();
}
SwitchMode::Random(SwitchInterval::PerLine) => {
current_dictionary_name = select_random_dictionary(config, false)?;
}
_ => {}
}
if !quiet {
if !no_color {
eprint!("\x1b[32mDictionary: {}\x1b[0m\r\n", current_dictionary_name);
} else {
eprint!("Dictionary: {}\r\n", current_dictionary_name);
}
}
continue; }
let term_width = match terminal_size::terminal_size() {
Some((terminal_size::Width(w), _)) => w as usize,
None => 80,
};
let display = match &built_dictionary {
BuiltDictionary::Char(dictionary) => {
let base = dictionary.base();
let bits_per_char = (base as f64).log2();
let bytes_per_line = ((term_width as f64 * bits_per_char) / 8.0).ceil() as usize;
let mut random_bytes = vec![0u8; bytes_per_line.max(1)];
use rand::RngCore;
rng.fill_bytes(&mut random_bytes);
let encoded = encode(&random_bytes, dictionary);
encoded.chars().take(term_width).collect::<String>()
}
BuiltDictionary::Word(word_dict) => {
generate_word_line(&mut rng, word_dict, term_width)
}
BuiltDictionary::Alternating(alt_dict) => {
generate_alternating_word_line(&mut rng, alt_dict, term_width)
}
};
print!("{}\r\n", display);
io::stdout().flush()?;
if poll(Duration::from_millis(25))?
&& let Event::Key(key_event) = read()?
{
match key_event.code {
KeyCode::Char('c')
if key_event
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL) =>
{
disable_raw_mode()?;
if !no_color {
print!("\x1b[0m"); }
std::process::exit(0);
}
KeyCode::Esc => {
disable_raw_mode()?;
if !no_color {
print!("\x1b[0m"); }
std::process::exit(0);
}
KeyCode::Char(' ') => {
current_dictionary_name = select_random_dictionary(config, false)?;
current_index = dict_names
.iter()
.position(|n| n == ¤t_dictionary_name)
.unwrap_or(0);
if !quiet {
if !no_color {
eprint!("\r\x1b[32m[Matrix: {}]\x1b[0m\r\n", current_dictionary_name);
} else {
eprint!("\r[Matrix: {}]\r\n", current_dictionary_name);
}
}
continue; }
KeyCode::Left => {
current_index = if current_index == 0 {
dict_names.len() - 1
} else {
current_index - 1
};
current_dictionary_name = dict_names[current_index].clone();
if !quiet {
if !no_color {
eprint!("\r\x1b[32m[Matrix: {}]\x1b[0m\r\n", current_dictionary_name);
} else {
eprint!("\r[Matrix: {}]\r\n", current_dictionary_name);
}
}
continue; }
KeyCode::Right => {
current_index = (current_index + 1) % dict_names.len();
current_dictionary_name = dict_names[current_index].clone();
if !quiet {
if !no_color {
eprint!("\r\x1b[32m[Matrix: {}]\x1b[0m\r\n", current_dictionary_name);
} else {
eprint!("\r[Matrix: {}]\r\n", current_dictionary_name);
}
}
continue; }
_ => {}
}
}
if !superman {
thread::sleep(Duration::from_millis(250));
}
}
}
pub fn detect_mode(
config: &DictionaryRegistry,
file: Option<&PathBuf>,
show_candidates: Option<usize>,
decompress: Option<&String>,
max_size: usize,
) -> Result<(), Box<dyn std::error::Error>> {
use base_d::DictionaryDetector;
let input = if let Some(file_path) = file {
let metadata = fs::metadata(file_path)?;
let file_size = metadata.len() as usize;
if max_size > 0 && file_size > max_size {
return Err(format!(
"File size ({} bytes) exceeds limit ({} bytes). Use --force to process anyway.",
file_size, max_size
)
.into());
}
fs::read_to_string(file_path)?
} else {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
if max_size > 0 && buffer.len() > max_size {
return Err(format!(
"Input size ({} bytes) exceeds maximum ({} bytes). Use --file with --force for large inputs.",
buffer.len(),
max_size
).into());
}
buffer
};
let detector = DictionaryDetector::new(config)?;
let matches = detector.detect(input.trim());
if matches.is_empty() {
eprintln!("Could not detect dictionary - no matches found.");
eprintln!("The input may not be encoded data, or uses an unknown dictionary.");
std::process::exit(1);
}
if let Some(n) = show_candidates {
println!("Top {} candidate dictionaries:\n", n);
for (i, m) in matches.iter().take(n).enumerate() {
println!(
"{}. {} (confidence: {:.1}%)",
i + 1,
m.name,
m.confidence * 100.0
);
}
return Ok(());
}
let best_match = &matches[0];
eprintln!(
"Detected: {} (confidence: {:.1}%)",
best_match.name,
best_match.confidence * 100.0
);
if best_match.confidence < 0.7 {
eprintln!("Warning: Low confidence detection. Results may be incorrect.");
}
let decoded = decode(input.trim(), &best_match.dictionary)?;
let output = if let Some(decompress_name) = decompress {
let algo = base_d::CompressionAlgorithm::from_str(decompress_name)?;
base_d::decompress(&decoded, algo)?
} else {
decoded
};
io::stdout().write_all(&output)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn streaming_decode(
config: &DictionaryRegistry,
decode_name: &str,
file: Option<&PathBuf>,
decompress: Option<String>,
hash: Option<String>,
xxhash_seed: Option<u64>,
xxhash_secret_stdin: bool,
encode: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
use base_d::StreamingDecoder;
let decode_dictionary = create_dictionary(config, decode_name)?;
let mut decoder = StreamingDecoder::new(&decode_dictionary, io::stdout());
if let Some(algo_name) = decompress {
let algo = base_d::CompressionAlgorithm::from_str(&algo_name)?;
decoder = decoder.with_decompression(algo);
}
if let Some(hash_name) = &hash {
let hash_algo = base_d::HashAlgorithm::from_str(hash_name)?;
decoder = decoder.with_hashing(hash_algo);
let xxhash_config =
load_xxhash_config(xxhash_seed, xxhash_secret_stdin, config, Some(&hash_algo))?;
decoder = decoder.with_xxhash_config(xxhash_config);
}
let hash_result = if let Some(file_path) = file {
let mut file_handle = fs::File::open(file_path)?;
decoder
.decode(&mut file_handle)
.map_err(|e| format!("{:?}", e))?
} else {
decoder
.decode(&mut io::stdin())
.map_err(|e| format!("{:?}", e))?
};
if let Some(hash_bytes) = hash_result {
if let Some(encode_name) = encode {
let encode_dictionary = create_dictionary(config, &encode_name)?;
let hash_encoded = base_d::encode(&hash_bytes, &encode_dictionary);
eprintln!("Hash: {}", hash_encoded);
} else {
eprintln!("Hash: {}", hex::encode(hash_bytes));
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn streaming_encode(
config: &DictionaryRegistry,
encode_name: &str,
file: Option<&PathBuf>,
compress: Option<String>,
level: Option<u32>,
hash: Option<String>,
xxhash_seed: Option<u64>,
xxhash_secret_stdin: bool,
) -> Result<(), Box<dyn std::error::Error>> {
use base_d::StreamingEncoder;
let encode_dictionary = create_dictionary(config, encode_name)?;
let mut encoder = StreamingEncoder::new(&encode_dictionary, io::stdout());
if let Some(algo_name) = compress {
let algo = base_d::CompressionAlgorithm::from_str(&algo_name)?;
let compression_level = get_compression_level(config, level, algo);
encoder = encoder.with_compression(algo, compression_level);
}
if let Some(hash_name) = &hash {
let hash_algo = base_d::HashAlgorithm::from_str(hash_name)?;
encoder = encoder.with_hashing(hash_algo);
let xxhash_config =
load_xxhash_config(xxhash_seed, xxhash_secret_stdin, config, Some(&hash_algo))?;
encoder = encoder.with_xxhash_config(xxhash_config);
}
let hash_result = if let Some(file_path) = file {
let mut file_handle = fs::File::open(file_path)?;
encoder
.encode(&mut file_handle)
.map_err(|e| format!("{}", e))?
} else {
encoder
.encode(&mut io::stdin())
.map_err(|e| format!("{}", e))?
};
if let Some(hash_bytes) = hash_result {
eprintln!("Hash: {}", hex::encode(hash_bytes));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_word_line_basic() {
let word_dict = base_d::WordDictionary::builder()
.words(vec!["abandon", "ability", "able", "about"])
.delimiter(" ")
.build()
.unwrap();
let mut rng = rand::rng();
let line = generate_word_line(&mut rng, &word_dict, 80);
assert!(!line.is_empty());
assert!(line.chars().count() <= 80);
for word in line.split(' ') {
assert!(
["abandon", "ability", "able", "about"].contains(&word),
"Unknown word in line: {}",
word
);
}
}
#[test]
fn test_generate_word_line_respects_width() {
let word_dict = base_d::WordDictionary::builder()
.words(vec![
"verylongword",
"anotherlongword",
"yetanotherlongword",
])
.delimiter(" ")
.build()
.unwrap();
let mut rng = rand::rng();
let line = generate_word_line(&mut rng, &word_dict, 20);
assert!(line.chars().count() <= 20);
let line = generate_word_line(&mut rng, &word_dict, 5);
assert!(line.chars().count() <= 5);
assert!(
!line.is_empty(),
"Should produce truncated word, not empty string"
);
}
#[test]
fn test_generate_word_line_truncates_long_first_word() {
let word_dict = base_d::WordDictionary::builder()
.words(vec!["supercalifragilisticexpialidocious"]) .delimiter(" ")
.build()
.unwrap();
let mut rng = rand::rng();
let line = generate_word_line(&mut rng, &word_dict, 10);
assert_eq!(line.chars().count(), 10);
assert_eq!(line, "supercalif");
}
#[test]
fn test_generate_word_line_empty_dictionary() {
}
#[test]
fn test_generate_word_line_with_custom_delimiter() {
let word_dict = base_d::WordDictionary::builder()
.words(vec!["alpha", "bravo", "charlie", "delta"])
.delimiter("-")
.build()
.unwrap();
let mut rng = rand::rng();
let line = generate_word_line(&mut rng, &word_dict, 80);
assert!(line.contains('-') || !line.contains(' '));
}
#[test]
fn test_generate_word_line_bip39_style() {
let bip39_sample = base_d::WordDictionary::builder()
.words(vec![
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract",
"absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
])
.delimiter(" ")
.build()
.unwrap();
let mut rng = rand::rng();
let line = generate_word_line(&mut rng, &bip39_sample, 100);
let word_count = line.split(' ').count();
assert!(word_count >= 1, "Should have at least one word");
for word in line.split(' ') {
assert!(
bip39_sample.decode_word(word).is_some(),
"Word not in dictionary: {}",
word
);
}
}
}