use crate::cli_pretty_printing;
use crate::cli_pretty_printing::decoded_how_many_times;
use crate::filtration_system::{
get_decoder_tagged_decoders, get_non_decoder_tagged_decoders, MyResults,
};
use crossbeam::channel::Sender;
use log::{debug, trace};
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashSet};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use crate::checkers::athena::Athena;
use crate::checkers::checker_type::{Check, Checker};
use crate::checkers::CheckerTypes;
use crate::config::get_config;
use crate::searchers::helper_functions::{
calculate_string_quality, check_if_string_cant_be_decoded, generate_heuristic,
update_decoder_stats,
};
use crate::storage::wait_athena_storage;
use crate::DecoderResult;
const PRUNE_THRESHOLD: usize = 100000;
const INITIAL_PRUNE_THRESHOLD: usize = PRUNE_THRESHOLD;
const MAX_DEPTH: u32 = 100;
#[derive(Debug)]
struct AStarNode {
state: DecoderResult,
cost: u32,
heuristic: f32,
total_cost: f32,
}
impl Ord for AStarNode {
fn cmp(&self, other: &Self) -> Ordering {
other
.total_cost
.partial_cmp(&self.total_cost)
.unwrap_or(Ordering::Equal)
}
}
impl PartialOrd for AStarNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for AStarNode {
fn eq(&self, other: &Self) -> bool {
self.total_cost == other.total_cost
}
}
impl Eq for AStarNode {}
pub fn astar(input: String, result_sender: Sender<Option<DecoderResult>>, stop: Arc<AtomicBool>) {
let initial_heuristic = generate_heuristic(&input, &[]);
let initial = DecoderResult {
text: vec![input],
path: vec![],
};
let mut seen_strings = HashSet::new();
let mut seen_count = 0;
let mut open_set = BinaryHeap::new();
open_set.push(AStarNode {
state: initial,
cost: 0,
heuristic: initial_heuristic,
total_cost: 0.0,
});
let mut curr_depth: u32 = 1;
let mut prune_threshold = INITIAL_PRUNE_THRESHOLD;
while !open_set.is_empty() && !stop.load(std::sync::atomic::Ordering::Relaxed) {
trace!(
"Current depth is {:?}, open set size: {}",
curr_depth,
open_set.len()
);
let current_node = open_set.pop().unwrap();
trace!(
"Processing node with cost {}, heuristic {}, total cost {}",
current_node.cost,
current_node.heuristic,
current_node.total_cost
);
if stop.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
let mut decoder_tagged_decoders = get_decoder_tagged_decoders(¤t_node.state);
if let Some(last_decoder) = current_node.state.path.last() {
if last_decoder.checker_description.contains("reciprocal") {
let excluded_name = last_decoder.decoder;
decoder_tagged_decoders
.components
.retain(|d| d.get_name() != excluded_name);
}
}
if !decoder_tagged_decoders.components.is_empty() {
trace!(
"Found {} decoder-tagged decoders to execute immediately",
decoder_tagged_decoders.components.len()
);
if stop.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
let athena_checker = Checker::<Athena>::new();
let checker = CheckerTypes::CheckAthena(athena_checker);
let decoder_results = decoder_tagged_decoders.run(¤t_node.state.text[0], checker);
match decoder_results {
MyResults::Break(res) => {
trace!("Found successful decoding with decoder-tagged decoder");
cli_pretty_printing::success(&format!(
"DEBUG: astar.rs - decoder-tagged decoder - res.success: {}",
res.success
));
if res.success {
let mut decoders_used = current_node.state.path.clone();
let text = res.unencrypted_text.clone().unwrap_or_default();
decoders_used.push(res.clone());
let result_text = DecoderResult {
text: text.clone(),
path: decoders_used,
};
decoded_how_many_times(curr_depth);
cli_pretty_printing::success(&format!("DEBUG: astar.rs - decoder-tagged decoder - Sending successful result with {} decoders", result_text.path.len()));
if get_config().top_results {
if let Some(plaintext) = text.first() {
let decoder_name =
if let Some(last_decoder) = result_text.path.last() {
last_decoder.decoder.to_string()
} else {
"Unknown".to_string()
};
let checker_name =
if let Some(last_decoder) = result_text.path.last() {
last_decoder.checker_name.to_string()
} else {
"Unknown".to_string()
};
if !checker_name.is_empty() && checker_name != "Unknown" {
log::trace!(
"Storing plaintext in WaitAthena storage: {} (decoder: {}, checker: {})",
plaintext,
decoder_name,
checker_name
);
wait_athena_storage::add_plaintext_result(
plaintext.clone(),
format!("Decoded successfully at depth {}", curr_depth),
checker_name,
decoder_name,
);
let results = wait_athena_storage::get_plaintext_results();
log::trace!(
"WaitAthena storage now has {} results",
results.len()
);
} else {
log::trace!(
"Skipping plaintext with empty or unknown checker name: {} (decoder: {})",
plaintext,
decoder_name
);
}
} else {
log::trace!(
"No plaintext to store in WaitAthena storage (decoder-tagged)"
);
}
}
result_sender
.send(Some(result_text))
.expect("Should successfully send the result");
if !get_config().top_results {
stop.store(true, std::sync::atomic::Ordering::Relaxed);
return;
}
} else {
trace!("Human checker rejected the result, continuing search");
}
}
MyResults::Continue(results_vec) => {
trace!(
"Processing {} results from decoder-tagged decoders",
results_vec.len()
);
for mut r in results_vec {
let mut decoders_used = current_node.state.path.clone();
let mut text = r.unencrypted_text.take().unwrap_or_default();
text.retain(|s| {
if check_if_string_cant_be_decoded(s) {
update_decoder_stats(r.decoder, false);
return false;
}
if seen_strings.insert(s.clone()) {
seen_count += 1;
if seen_count > prune_threshold {
debug!(
"Pruning seen_strings HashSet (size: {})",
seen_strings.len()
);
let mut quality_scores: Vec<(String, f32)> = seen_strings
.iter()
.map(|s| (s.clone(), calculate_string_quality(s)))
.collect();
quality_scores.sort_by(|a, b| {
b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)
});
let keep_count = seen_strings.len() / 2;
let strings_to_keep: HashSet<String> = quality_scores
.into_iter()
.take(keep_count)
.map(|(s, _)| s)
.collect();
seen_strings = strings_to_keep;
seen_count = seen_strings.len();
let progress_factor = curr_depth as f32 / MAX_DEPTH as f32;
prune_threshold = INITIAL_PRUNE_THRESHOLD
- (progress_factor * 5000.0) as usize;
debug!(
"Pruned to {} high-quality entries (new threshold: {})",
seen_count, prune_threshold
);
}
true
} else {
false
}
});
if text.is_empty() {
update_decoder_stats(r.decoder, false);
continue;
}
decoders_used.push(r.clone());
let cost = current_node.cost + 1;
let heuristic = generate_heuristic(&text[0], &decoders_used);
let total_cost = cost as f32 + heuristic;
let new_node = AStarNode {
state: DecoderResult {
text,
path: decoders_used,
},
cost,
heuristic,
total_cost,
};
open_set.push(new_node);
update_decoder_stats(r.decoder, true);
}
}
}
}
let mut non_decoder_decoders = get_non_decoder_tagged_decoders(¤t_node.state);
if let Some(last_decoder) = current_node.state.path.last() {
if last_decoder.checker_description.contains("reciprocal") {
let excluded_name = last_decoder.decoder;
non_decoder_decoders
.components
.retain(|d| d.get_name() != excluded_name);
}
}
if !non_decoder_decoders.components.is_empty() {
trace!(
"Processing {} non-decoder-tagged decoders",
non_decoder_decoders.components.len()
);
if stop.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
let athena_checker = Checker::<Athena>::new();
let checker = CheckerTypes::CheckAthena(athena_checker);
let decoder_results = non_decoder_decoders.run(¤t_node.state.text[0], checker);
match decoder_results {
MyResults::Break(res) => {
trace!("Found successful decoding with non-decoder-tagged decoder");
cli_pretty_printing::success(&format!(
"DEBUG: astar.rs - non-decoder-tagged decoder - res.success: {}",
res.success
));
if res.success {
let mut decoders_used = current_node.state.path.clone();
let text = res.unencrypted_text.clone().unwrap_or_default();
decoders_used.push(res.clone());
let result_text = DecoderResult {
text: text.clone(),
path: decoders_used,
};
decoded_how_many_times(curr_depth);
cli_pretty_printing::success(&format!("DEBUG: astar.rs - non-decoder-tagged decoder - Sending successful result with {} decoders", result_text.path.len()));
if get_config().top_results {
if let Some(plaintext) = text.first() {
let decoder_name =
if let Some(last_decoder) = result_text.path.last() {
last_decoder.decoder.to_string()
} else {
"Unknown".to_string()
};
let checker_name =
if let Some(last_decoder) = result_text.path.last() {
last_decoder.checker_name.to_string()
} else {
"Unknown".to_string()
};
if !checker_name.is_empty() && checker_name != "Unknown" {
log::trace!(
"Storing plaintext in WaitAthena storage: {} (decoder: {}, checker: {})",
plaintext,
decoder_name,
checker_name
);
wait_athena_storage::add_plaintext_result(
plaintext.clone(),
format!("Decoded successfully at depth {}", curr_depth),
checker_name,
decoder_name,
);
let results = wait_athena_storage::get_plaintext_results();
log::trace!(
"WaitAthena storage now has {} results",
results.len()
);
} else {
log::trace!(
"Skipping plaintext with empty or unknown checker name: {} (decoder: {})",
plaintext,
decoder_name
);
}
} else {
log::trace!("No plaintext to store in WaitAthena storage (non-decoder-tagged)");
}
}
result_sender
.send(Some(result_text))
.expect("Should successfully send the result");
if !get_config().top_results {
stop.store(true, std::sync::atomic::Ordering::Relaxed);
return;
}
} else {
trace!("Human checker rejected the result, continuing search");
}
}
MyResults::Continue(results_vec) => {
trace!(
"Processing {} results from non-decoder-tagged decoders",
results_vec.len()
);
for mut r in results_vec {
let mut decoders_used = current_node.state.path.clone();
let mut text = r.unencrypted_text.take().unwrap_or_default();
text.retain(|s| {
if check_if_string_cant_be_decoded(s) {
update_decoder_stats(r.decoder, false);
return false;
}
if seen_strings.insert(s.clone()) {
seen_count += 1;
if seen_count > prune_threshold {
debug!(
"Pruning seen_strings HashSet (size: {})",
seen_strings.len()
);
let mut quality_scores: Vec<(String, f32)> = seen_strings
.iter()
.map(|s| (s.clone(), calculate_string_quality(s)))
.collect();
quality_scores.sort_by(|a, b| {
b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)
});
let keep_count = seen_strings.len() / 2;
let strings_to_keep: HashSet<String> = quality_scores
.into_iter()
.take(keep_count)
.map(|(s, _)| s)
.collect();
seen_strings = strings_to_keep;
seen_count = seen_strings.len();
let progress_factor = curr_depth as f32 / MAX_DEPTH as f32;
prune_threshold = INITIAL_PRUNE_THRESHOLD
- (progress_factor * 5000.0) as usize;
debug!(
"Pruned to {} high-quality entries (new threshold: {})",
seen_count, prune_threshold
);
}
true
} else {
false
}
});
if text.is_empty() {
update_decoder_stats(r.decoder, false);
continue;
}
decoders_used.push(r.clone());
let cost = current_node.cost + 1;
let heuristic = generate_heuristic(&text[0], &decoders_used);
let total_cost = cost as f32 + heuristic;
let new_node = AStarNode {
state: DecoderResult {
text,
path: decoders_used,
},
cost,
heuristic,
total_cost,
};
open_set.push(new_node);
update_decoder_stats(r.decoder, true);
}
}
}
}
curr_depth += 1;
}
if stop.load(std::sync::atomic::Ordering::Relaxed) {
trace!("A* search stopped by external signal");
} else {
trace!("A* search completed without finding a solution");
result_sender.try_send(None).ok();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossbeam::channel::bounded;
#[test]
fn astar_handles_empty_input() {
let (tx, rx) = bounded::<Option<DecoderResult>>(1);
let stopper = Arc::new(AtomicBool::new(false));
astar("".into(), tx, stopper);
let result = rx.recv().unwrap();
assert!(result.is_none());
}
#[test]
fn astar_prevents_cycles() {
let (tx, rx) = bounded::<Option<DecoderResult>>(1);
let stopper = Arc::new(AtomicBool::new(false));
astar("aGVsbG8=".into(), tx, stopper);
let result = rx.recv().unwrap();
assert!(result.is_some());
}
}