rusty-diceware

Commandline diceware, with or without dice, written in Rustlang.
git clone https://kaka.farm/~git/rusty-diceware
Log | Files | Refs | README | LICENSE

commit 5e124728c690569cf45213179d8cdf6a29dd9890
parent 833b4ac8572d6b202b2cef563a6acc2b6ecaa79a
Author: Yuval Langer <yuval.langer@gmail.com>
Date:   Mon,  4 Nov 2024 02:22:07 +0200

Rewrite the main function hopefully fix the asks too many dice rolls bug.

Diffstat:
Msrc/main.rs | 314+++++++++++++++++++++++++++++++++++++++++--------------------------------------
1 file changed, 164 insertions(+), 150 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -14,22 +14,23 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ + */ extern crate getopts; extern crate rand; use std::fs::File; -use std::io::BufRead; +// use std::io::BufRead; use std::io::Read; use std::process::exit; use std::str::FromStr; use diceware_wordlists::Wordlist; use getopts::Options; -use rand::prelude::SliceRandom; +// use rand::prelude::SliceRandom; use rand::rngs::ThreadRng; use rand::thread_rng; +use crate::rand::Rng; fn make_options() -> Options { let mut opts = Options::new(); @@ -66,126 +67,144 @@ fn unknown_wordlist(wordlist_name: &str) -> ! { exit(-1) } -fn find_number_of_rolls_needed(wordlist_length: u32) -> u32 { - let mut x = wordlist_length as u64; - let mut nrolls: u32 = 0; - while x != 0 { - x /= 6; - nrolls += 1; - } - nrolls -} - -fn rolls_to_word_index(number_of_rolls_needed: u32, rolls: &[u8]) -> usize { - if number_of_rolls_needed == rolls.len() as u32 { - let mut word_number = 0; - for (i, roll) in rolls.iter().rev().enumerate() { - if *roll < 1 || *roll > 6 { - panic!( - "Must be a die roll result between and including 1 and 6, not {}", - roll - ); - } - word_number += ((roll - 1) as usize) * 6_usize.pow(i as u32); - } - word_number - } else { - panic!( - "Wrong number of die casts: {}. Needs to be {} die casts.", - rolls.len(), - number_of_rolls_needed - ) - } -} - -fn print_words_rolls( - wordlist: &[&str], - delimiter: &char, - is_entropy_printed: &bool, - rolls: &[&[u8]], +fn print_words( + words: &Vec<String>, + delimiter: char, ) { - let number_of_rolls_needed = find_number_of_rolls_needed(wordlist.len() as u32); - for roll_index in 0..(rolls.len() - 1) { - let roll_sum = rolls_to_word_index( - number_of_rolls_needed, - rolls - .get(roll_index) - .unwrap_or_else(|| panic!("Bad roll index: {}", roll_index)), - ); - let word = wordlist - .get(roll_sum) - .unwrap_or_else(|| panic!("Wrong word index: {}", roll_sum)); - print!("{}{}", &word, delimiter); + for word in 0..(words.len() - 1) { + print!("{}{}", words[word], delimiter); } - let roll_sum = rolls_to_word_index(number_of_rolls_needed, rolls.last().unwrap()); - let word = wordlist.get(roll_sum).unwrap(); - print!("{}", &word); + print!("{}", words[words.len() - 1]); println!(); - if *is_entropy_printed { - println!("{}", entropyn(wordlist, rolls.len() as u64)) - } } -fn read_rolls() -> Vec<Vec<u8>> { +fn read_single_word(bytes_iter: &mut std::iter::Peekable<std::io::Bytes<std::io::Stdin>>, wordlist: &Vec<&str>) -> Option<String> { + eprintln!("##### wordlist length: {}", wordlist.len()); + let die_cast_per_word = die_cast_per_word(wordlist.len()); + eprintln!("##### die cast per word: {}", die_cast_per_word); + let mut word_index = 0; + + for die_significance in 0..=die_cast_per_word { + if die_significance == die_cast_per_word { + let current_byte_result = match bytes_iter.next() { + Some(x) => x, + None => { + eprintln!("Each roll must end with a newline."); + exit(-1); + } + }; + + match current_byte_result { + Ok(b'\n') => { + return Some(wordlist[word_index].to_string()); + }, + Ok(n) => match n { + b'1' | b'2' | b'3' | b'4' | b'5' | b'6' => { + eprintln!("Too many rolls. Each word must have exactly {} die rolls.", + die_cast_per_word); + exit(-1); + }, + err => { + eprintln!("Input must be either 1 to 6 inclusive digits or newline. (Got: ascii code {})", err); + exit(-1); + }, + }, + Err(err) => { + eprintln!("Unknown error: {}", err); + exit(-1); + } + }; + } else { + let current_byte_result = match bytes_iter.next() { + Some(x) => { + eprintln!("##### {:?}", x); + x + }, + None => { + eprintln!("Bad number of rolls. Each word must have exactly {} die rolls.", + die_cast_per_word); + exit(-1); + }, + }; + + word_index += match current_byte_result { + Ok(b'1') => 0 * 6_usize.pow(die_significance), + Ok(b'2') => 1 * 6_usize.pow(die_significance), + Ok(b'3') => 2 * 6_usize.pow(die_significance), + Ok(b'4') => 3 * 6_usize.pow(die_significance), + Ok(b'5') => 4 * 6_usize.pow(die_significance), + Ok(b'6') => 5 * 6_usize.pow(die_significance), + Ok(b'\n') => { + eprintln!("Bad number of rolls. Each word must have exactly {} die rolls.", + die_cast_per_word); + exit(-1); + }, + Ok(x) => { + eprintln!("Input must be either 1 to 6 inclusive digits or newline. (Got ascii code {})", x); + exit(-1); + }, + Err(err) => { + eprintln!("Error while reading die casts: {}", err); + exit(-1); + }, + }; + }; + }; + + return Some(wordlist[word_index].to_string()); +} + +fn read_words_from_rolls(wordlist: &Vec<&str>) -> Vec<String> { let stdin = std::io::stdin(); - let mut rolls: Vec<Vec<u8>> = Vec::new(); - let mut last_number_of_dice = None; - for line in stdin.lock().lines() { - let line_value = line.unwrap(); - let line_value_trimmed = line_value.trim(); - if line_value_trimmed.is_empty() { - continue; - } - let current_number_of_dice = line_value_trimmed.len(); - if let Some(last_number_of_dice_value) = last_number_of_dice { - if last_number_of_dice_value != current_number_of_dice { - panic!("Not all dice rolls were of the same number of die."); - } else { - last_number_of_dice = Some(current_number_of_dice); - } + let mut words: Vec<String> = Vec::new(); + let mut bytes_iter = stdin.bytes().peekable(); + + loop { + match read_single_word(&mut bytes_iter, wordlist) { + Some(word) => words.push(word), + None => break, + }; + + match bytes_iter.peek() { + None => break, + Some(_) => continue, } + }; - rolls.push( - line_value_trimmed - .chars() - .map(|c| { - c.to_string() - .parse() - .unwrap_or_else(|e| panic!("Not a digit: \"{}\". Error: {}", c, e)) - }) - .collect(), - ); - } - rolls + words } fn entropy(wordlist: &[&str]) -> f64 { (wordlist.len() as f64).log2() } -fn entropyn(wordlist: &[&str], n: u64) -> f64 { - entropy(wordlist) * (n as f64) +fn entropyn(wordlist: Vec<&str>, n: u64) -> f64 { + entropy(&wordlist) * (n as f64) } -fn print_words_rng( - wordlist: &[&str], - word_num: &u64, - delimiter: &char, - is_entropy_printed: &bool, +fn read_rng_rolls( rng: &mut ThreadRng, -) { - for _ in 0..(word_num - 1) { - let word = wordlist.choose(rng).unwrap(); - print!("{}{}", &word, delimiter); - } - let word = wordlist.choose(rng).unwrap(); - print!("{}", word); - - println!(); - if *is_entropy_printed { - println!("{}", entropyn(wordlist, *word_num)) - } + wordlist: &Vec<&str>, + word_num: u64, +) -> Vec<String> { + let die_cast_per_word = die_cast_per_word(wordlist.len()); + + /* + From base 6 conversion: + (0..6).pow(1) = (0, 1, ..6) + (0..6).pow(2) = (0, 6, ..36) + (0..6).pow(3) = (0, 36, ..216) + */ + return (0..word_num) + .collect::<Vec<_>>() + .iter().map(|_| { + let current_word_index: usize = (0..die_cast_per_word).collect::<Vec<_>>() + .into_iter() + .map(|die_significance| (rng.gen_range(0..6_usize)) * ((6_usize).pow(die_significance))) + .sum(); + wordlist[current_word_index].to_string() + }).collect(); } fn load_wordlist_file(filepath: &str) -> String { @@ -200,6 +219,10 @@ fn load_wordlist_file(filepath: &str) -> String { wordlist_string } +fn die_cast_per_word(wordlist_length: usize) -> u32 { + wordlist_length.ilog(6) +} + fn main() { let args: Vec<String> = std::env::args().collect(); let program = &args[0]; @@ -241,54 +264,45 @@ fn main() { Wordlist::default() }; - let mut rng = thread_rng(); - - if let Some(wordlist_filepath) = matches.opt_str("f") { - let wordlist_string = load_wordlist_file(&wordlist_filepath); - let wordlist = wordlist_string + let wordlist_string; + + let wordlist = if let Some(wordlist_filepath) = matches.opt_str("f") { + wordlist_string = load_wordlist_file(&wordlist_filepath); + wordlist_string .split('\n') .map(|x| x.trim()) .filter(|x| x != &"") - .collect::<Vec<&str>>(); - - if is_physical_rolls { - let rolls = read_rolls(); - - print_words_rolls( - &wordlist, - &delimiter, - &is_entropy_printed, - &rolls.iter().map(|x| x.as_ref()).collect::<Vec<&[u8]>>(), - ); - } else if word_num != 0 { - print_words_rng( - &wordlist, - &word_num, - &delimiter, - &is_entropy_printed, - &mut rng, - ); - }; + .collect::<Vec<&str>>() } else { - let wordlist = wordlist_name.get_list(); - - if is_physical_rolls { - let rolls = read_rolls(); - - print_words_rolls( - wordlist, - &delimiter, - &is_entropy_printed, - &rolls.iter().map(|x| x.as_ref()).collect::<Vec<&[u8]>>(), - ); - } else if word_num != 0 { - print_words_rng( - wordlist, - &word_num, - &delimiter, - &is_entropy_printed, - &mut rng, - ); - }; + wordlist_name.get_list().to_vec() + }; + + let words = if is_physical_rolls { + read_words_from_rolls(&wordlist) + } else if word_num != 0 { + let mut rng = thread_rng(); + read_rng_rolls(&mut rng, &wordlist, word_num) + } else { + print_usage(program, opts); + exit(-1); }; + + if words.len() > 0 { + print_words(&words, delimiter); + } + + if is_entropy_printed { + println!("{}", entropyn(wordlist, words.len() as u64)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rolls_to_word_index_01() { + assert_eq!(0, rolls_to_word_index(3, &[1, 1, 1])); + assert_eq!(1, rolls_to_word_index(3, &[1, 1, 2])); + } }