use block_modes::{BlockMode, Cbc};
use block_padding::ZeroPadding;
use chrono::format::strftime::StrftimeItems;
use chrono::{Datelike, Duration, NaiveDate};
use des::Des;
use regex::Regex;
use std::collections::HashMap;
use std::error::Error;
use std::mem::replace;
struct DateRange(NaiveDate, NaiveDate);
impl Iterator for DateRange {
type Item = NaiveDate;
fn next(&mut self) -> Option<Self::Item> {
if self.0 <= self.1 {
let next = self.0 + Duration::days(1);
Some(replace(&mut self.0, next))
} else {
None
}
}
}
fn first_pass(date: &str) -> Vec<i32> {
use vals::TABLE1;
let naive_date = NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap();
let date_components: Vec<i32> = date
.split('-')
.map(|i| i.parse::<i32>().expect("Error parsing date string"))
.collect();
let year = date_components[0];
let year_trimmed = year.to_string()[2..].parse::<i32>().unwrap();
let month = date_components[1];
let day = date_components[2];
let day_of_week = naive_date.weekday().num_days_from_monday() as usize;
let result: Vec<i32> = (0..8)
.map(|i| match i {
0..=4 => TABLE1[day_of_week][i],
5 => day,
6 => {
if ((year_trimmed + month) - day) < 0 {
(((year_trimmed + month) - day) + 36) % 36
} else {
((year_trimmed + month) - day) % 36
}
}
_ => (((3 + ((year_trimmed + month) % 12)) * day) % 37) % 36,
})
.collect();
return result;
}
fn second_pass(padded_seed: &str) -> Vec<i32> {
let result: Vec<i32> = padded_seed.chars().map(|c| c as i32).collect();
return result;
}
fn third_pass(first_result: Vec<i32>, second_result: Vec<i32>) -> Vec<i32> {
let first_eight: Vec<i32> = (0..8)
.map(|i| (first_result[i] + second_result[i]) % 36)
.collect();
let sum_of_parts: i32 = first_eight.iter().sum();
let mut result: Vec<i32> = Vec::from(first_eight);
result.push(sum_of_parts % 36);
let last_value = (result[8] % 6).pow(2) as f64;
if (last_value - last_value.floor()) < 0.5 {
result.push(last_value.floor() as i32)
} else {
result.push(last_value.ceil() as i32);
}
return result;
}
fn fourth_pass(third_result: Vec<i32>) -> Vec<i32> {
use vals::TABLE2;
let result: Vec<i32> = (0..10)
.map(|i| third_result[TABLE2[(third_result[8] % 6) as usize][i] as usize])
.collect();
return result;
}
fn fifth_pass(padded_seed: &str, fourth_result: Vec<i32>) -> String {
use vals::ALPHANUM;
let vec_a: Vec<i32> = padded_seed
.chars()
.enumerate()
.map(|(i, c)| (c as i32 + fourth_result[i]) % 36)
.collect();
let mut vec_b: Vec<char> = Vec::new();
for i in 0..10 {
vec_b.push(ALPHANUM[vec_a[i as usize] as usize]);
}
return vec_b.into_iter().collect();
}
fn derive_from_input(date: &str, padded_seed: &str) -> String {
let first_result = first_pass(date);
let second_result = second_pass(padded_seed);
let third_result = third_pass(first_result, second_result);
let fourth_result = fourth_pass(third_result);
let fifth_result = fifth_pass(padded_seed, fourth_result);
return fifth_result;
}
fn validate_seed(seed: &str) -> Result<String, Box<dyn Error>> {
use vals::DEFAULT_SEED;
if seed == DEFAULT_SEED {
return Ok(seed.to_string())
}
if seed.len() < 4 || seed.len() > 8 {
Err("Seed should be >= 4 and <= 8 characters long.")?;
}
let mut padded_seed = seed.to_string();
padded_seed.push_str(&seed[0..10 - &seed.len()]);
return Ok(padded_seed);
}
fn validate_date(date: &str) -> Result<bool, Box<dyn Error>> {
let date_regex: Regex = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
if !date_regex.is_match(date) {
Err("Invalid date format, must be YYYY-MM-DD")?;
}
Ok(true)
}
fn validate_range(date_begin: &str, date_end: &str) -> Result<bool, Box<dyn Error>> {
let naive_begin = NaiveDate::parse_from_str(date_begin, "%Y-%m-%d").unwrap();
let naive_end = NaiveDate::parse_from_str(date_end, "%Y-%m-%d").unwrap();
if naive_end.signed_duration_since(naive_begin) <= Duration::zero() {
Err("Invalid date range. Beginning date must occur before end date, and the values cannot be the same.")?;
}
if naive_end - naive_begin > Duration::days(365) {
Err("Invalid date range. Official tooling does not allow a date range exceeding 1 year.")?;
}
return Ok(true)
}
pub fn generate(date: &str, seed: &str) -> Result<String, Box<dyn Error>> {
let valid_date = validate_date(date);
if valid_date.is_err() {
Err(valid_date.unwrap_err())?;
}
let valid_seed = validate_seed(seed);
if valid_seed.is_err() {
let err_str = &valid_seed.as_ref();
Err(err_str.unwrap_err().to_string())?;
}
return Ok(derive_from_input(date, &valid_seed.unwrap()));
}
pub fn generate_multiple(
date_begin: &str,
date_end: &str,
seed: &str,
) -> Result<HashMap<String, String>, Box<dyn Error>> {
let valid_begin = validate_date(date_begin);
let valid_end = validate_date(date_end);
if valid_begin.is_err() {
Err(valid_begin.unwrap_err())?;
}
if valid_end.is_err() {
Err(valid_end.unwrap_err())?;
}
let valid_range = validate_range(date_begin, date_end);
if valid_range.is_err() {
Err(valid_range.unwrap_err())?;
}
let naive_begin = NaiveDate::parse_from_str(date_begin, "%Y-%m-%d").unwrap();
let naive_end = NaiveDate::parse_from_str(date_end, "%Y-%m-%d").unwrap();
let date_range = DateRange(naive_begin, naive_end);
let valid_seed = validate_seed(seed);
if valid_seed.is_err() {
let err_str = &valid_seed.as_ref();
Err(err_str.unwrap_err().to_string())?;
}
let mut potd_map = HashMap::new();
for date in date_range {
let format = StrftimeItems::new("%Y-%m-%d");
let date_string = date.format_with_items(format).to_string();
potd_map.insert(date_string.to_string(), derive_from_input(&date_string, &valid_seed.as_ref().unwrap()));
}
return Ok(potd_map);
}
pub fn seed_to_des(seed: &str) -> Result<String, Box<dyn Error>> {
use vals::DEFAULT_SEED;
if seed.len() < 4 || seed.len() > 8 {
Err("Seed should be >= 4 and <= 8 characters long.")?;
}
let default_des: String = "DB.B5.CB.D6.11.17.D6.EB".to_string();
if seed == DEFAULT_SEED {
Ok(default_des)
} else {
let key = [20, 157, 64, 213, 193, 46, 85, 2];
let iv = [0, 0, 0, 0, 0, 0, 0, 0];
type DesCbc = Cbc<Des, ZeroPadding>;
let cipher = DesCbc::new_from_slices(&key, &iv).unwrap();
let mut seed_buffer = [0u8; 8];
seed_buffer[..seed.len()].copy_from_slice(seed.as_bytes());
let encrypted_seed = cipher.encrypt(&mut seed_buffer, seed.len()).unwrap();
let seed_string: String = encrypted_seed
.iter()
.map(|i| {
if i == &encrypted_seed[7] {
format!("{:X}", i)
} else {
format!("{:X}.", i)
}
})
.collect();
Ok(seed_string)
}
}
#[cfg(test)]
mod tests;
pub mod vals;