feat: Engine, ThreadPool, and Progress display

ThreadPool: jthread-based work partitioning with SharedState atomics.
Progress: rich terminal output with Unicode box drawing, ANSI colors,
progress bar, speed/ETA display, and cracked/exhausted result rendering.
Engine: template function wiring hasher + attack + threading + salt.
3 integration tests passing (crack, exhaust, salted crack).
This commit is contained in:
CarterPerez-dev 2026-03-22 10:29:06 -04:00
parent 6542a097f8
commit 3e2c3ff002
4 changed files with 377 additions and 14 deletions

View File

@ -0,0 +1,148 @@
// ©AngelaMos | 2026
// Engine.hpp
#pragma once
#include <chrono>
#include <cstdio>
#include <expected>
#include <string>
#include <thread>
#include "src/attack/BruteForceAttack.hpp"
#include "src/attack/DictionaryAttack.hpp"
#include "src/attack/RuleAttack.hpp"
#include "src/config/Config.hpp"
#include "src/core/Concepts.hpp"
#include "src/display/Progress.hpp"
#include "src/threading/ThreadPool.hpp"
class Engine {
public:
template <Hasher H, AttackStrategy A>
static auto crack(const CrackConfig& cfg)
-> std::expected<CrackResult, CrackError>;
};
template <Hasher H, AttackStrategy A>
auto Engine::crack(const CrackConfig& cfg)
-> std::expected<CrackResult, CrackError> {
unsigned thread_count = cfg.thread_count > 0
? cfg.thread_count
: std::thread::hardware_concurrency();
ThreadPool pool(thread_count);
auto attack_name = [&]() -> std::string_view {
if (cfg.bruteforce) { return "Brute Force"; }
if (cfg.use_rules) { return "Rules"; }
return "Dictionary";
}();
auto total_estimate = [&]() -> std::size_t {
if constexpr (std::same_as<A, BruteForceAttack>) {
BruteForceAttack probe(cfg.charset, cfg.max_length, 0, 1);
return probe.total();
} else if constexpr (std::same_as<A, RuleAttack>) {
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
if (!probe) { return 0; }
return probe->total() * 2005;
} else {
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
if (!probe) { return 0; }
return probe->total();
}
}();
Progress progress(H::name(), attack_name, thread_count,
total_estimate, pool.state().found,
pool.state().tested_count);
if (Progress::is_tty()) {
progress.print_banner();
std::puts("");
std::puts("");
std::puts("");
}
auto start = std::chrono::steady_clock::now();
std::jthread display_thread;
if (Progress::is_tty()) {
display_thread = std::jthread([&](std::stop_token st) {
while (!st.stop_requested() &&
!pool.state().found.load(std::memory_order_relaxed)) {
progress.update();
std::this_thread::sleep_for(
std::chrono::milliseconds(config::PROGRESS_UPDATE_MS));
}
});
}
pool.run([&](unsigned tid, unsigned total, SharedState& state) {
H hasher;
auto create_attack = [&]() {
if constexpr (std::same_as<A, BruteForceAttack>) {
return std::expected<BruteForceAttack, CrackError>(
BruteForceAttack(cfg.charset, cfg.max_length, tid, total));
} else if constexpr (std::same_as<A, RuleAttack>) {
return RuleAttack::create(
cfg.wordlist_path, cfg.chain_rules, tid, total);
} else {
return DictionaryAttack::create(cfg.wordlist_path, tid, total);
}
};
auto attack = create_attack();
if (!attack.has_value()) { return; }
while (!state.found.load(std::memory_order_relaxed)) {
auto candidate = attack->next();
if (!candidate.has_value()) { break; }
std::string to_hash = *candidate;
if (!cfg.salt.empty()) {
if (cfg.salt_position == "prepend") {
to_hash = cfg.salt + to_hash;
} else {
to_hash = to_hash + cfg.salt;
}
}
if (hasher.hash(to_hash) == cfg.target_hash) {
state.set_result(std::move(*candidate));
break;
}
state.tested_count.fetch_add(1, std::memory_order_relaxed);
}
});
if (display_thread.joinable()) {
display_thread.request_stop();
display_thread.join();
}
auto end = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(end - start).count();
auto tested = pool.state().tested_count.load(std::memory_order_relaxed);
double speed = (elapsed > 0.0) ? static_cast<double>(tested) / elapsed : 0.0;
auto& state = pool.state();
if (state.found.load(std::memory_order_relaxed) && state.result.has_value()) {
CrackResult result{
.plaintext = *state.result,
.hash = cfg.target_hash,
.algorithm = std::string(H::name()),
.elapsed_seconds = elapsed,
.candidates_tested = tested,
.hashes_per_second = speed
};
progress.print_cracked(result);
return result;
}
progress.print_exhausted(cfg.target_hash, H::name());
return std::unexpected(CrackError::Exhausted);
}

View File

@ -3,6 +3,12 @@
#include "src/display/Progress.hpp"
#include "src/config/Config.hpp"
#include <chrono>
#include <print>
#include <sys/ioctl.h>
#include <unistd.h>
static auto start_time = std::chrono::steady_clock::now();
Progress::Progress(std::string_view algorithm, std::string_view attack_mode,
unsigned thread_count, std::size_t total_candidates,
@ -10,15 +16,178 @@ Progress::Progress(std::string_view algorithm, std::string_view attack_mode,
const std::atomic<std::size_t>& tested)
: algorithm_(algorithm), attack_mode_(attack_mode),
thread_count_(thread_count), total_(total_candidates),
found_(found), tested_(tested) {}
found_(found), tested_(tested) {
start_time = std::chrono::steady_clock::now();
}
void Progress::print_banner() const {}
void Progress::update() {}
void Progress::print_cracked(const CrackResult&) const {}
void Progress::print_exhausted(std::string_view, std::string_view) const {}
bool Progress::is_tty() { return false; }
std::size_t Progress::terminal_width() { return 80; }
std::string Progress::render_bar(double, std::size_t) const { return ""; }
std::string Progress::format_count(std::size_t) { return ""; }
std::string Progress::format_time(double) { return ""; }
std::string Progress::format_speed(double) { return ""; }
bool Progress::is_tty() {
return isatty(STDOUT_FILENO) != 0;
}
std::size_t Progress::terminal_width() {
struct winsize ws{};
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
return ws.ws_col;
}
return 80;
}
std::string Progress::format_count(std::size_t n) {
if (n >= 1'000'000'000) {
return std::format("{:.1f}B", static_cast<double>(n) / 1'000'000'000.0);
}
if (n >= 1'000'000) {
return std::format("{:.1f}M", static_cast<double>(n) / 1'000'000.0);
}
if (n >= 1'000) {
return std::format("{:.1f}K", static_cast<double>(n) / 1'000.0);
}
return std::format("{}", n);
}
std::string Progress::format_time(double seconds) {
auto mins = static_cast<int>(seconds) / 60;
auto secs = seconds - static_cast<double>(mins * 60);
return std::format("{:02d}:{:05.2f}", mins, secs);
}
std::string Progress::format_speed(double hps) {
return format_count(static_cast<std::size_t>(hps)) + " h/s";
}
std::string Progress::render_bar(double fraction, std::size_t width) const {
if (width < config::PROGRESS_BAR_MIN_WIDTH) {
width = config::PROGRESS_BAR_MIN_WIDTH;
}
auto filled = static_cast<std::size_t>(fraction * static_cast<double>(width));
if (filled > width) { filled = width; }
std::string bar;
bar += config::box::BAR_LEFT;
for (std::size_t i = 0; i < width; ++i) {
bar += (i < filled) ? config::box::BLOCK_FULL : config::box::BLOCK_EMPTY;
}
bar += config::box::BAR_RIGHT;
return bar;
}
void Progress::print_banner() const {
if (!is_tty()) { return; }
auto w = terminal_width();
auto inner_width = (w > 6) ? w - 6 : 40;
std::string top_border(inner_width, '\0');
std::string bot_border(inner_width, '\0');
top_border.clear();
bot_border.clear();
for (std::size_t i = 0; i < inner_width; ++i) {
top_border += config::box::HORIZONTAL;
bot_border += config::box::HORIZONTAL;
}
auto line1 = std::format(" {} {} v{}",
config::APP_NAME, config::box::VERTICAL, config::VERSION);
auto line2 = std::format(" {} {} {} {} {} threads",
config::box::VERTICAL, algorithm_, config::box::VERTICAL,
attack_mode_, thread_count_);
std::println("{}{}{}{}{}",
config::color::CYAN, config::box::TOP_LEFT,
top_border, config::box::TOP_RIGHT, config::color::RESET);
std::println("{}{} {:<{}}{}{}", config::color::CYAN,
config::box::VERTICAL, line1, inner_width - 1,
config::box::VERTICAL, config::color::RESET);
std::println("{}{} {:<{}}{}{}", config::color::CYAN,
config::box::VERTICAL, line2, inner_width - 1,
config::box::VERTICAL, config::color::RESET);
std::println("{}{}{}{}{}",
config::color::CYAN, config::box::BOTTOM_LEFT,
bot_border, config::box::BOTTOM_RIGHT, config::color::RESET);
std::println("");
}
void Progress::update() {
if (!is_tty()) { return; }
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - start_time).count();
auto tested_val = tested_.load(std::memory_order_relaxed);
double fraction = (total_ > 0)
? static_cast<double>(tested_val) / static_cast<double>(total_)
: 0.0;
if (fraction > 1.0) { fraction = 1.0; }
double speed = (elapsed > 0.0)
? static_cast<double>(tested_val) / elapsed
: 0.0;
double eta = (speed > 0.0 && total_ > tested_val)
? static_cast<double>(total_ - tested_val) / speed
: 0.0;
auto bar_width = terminal_width();
bar_width = (bar_width > 30) ? bar_width - 20 : 10;
std::print("\033[3A");
std::println(" {}{} {:.1f}%{}",
config::color::YELLOW, render_bar(fraction, bar_width),
fraction * 100.0, config::color::RESET);
std::println(" {} {} {} {} {} {} {} ~{}{}",
config::icon::BOLT, config::color::CYAN,
format_speed(speed), config::color::RESET,
config::icon::TIMER, config::color::CYAN,
format_time(elapsed), format_time(eta),
config::color::RESET);
std::println(" {} {} {} / {} candidates{}",
config::icon::CHART, config::color::CYAN,
format_count(tested_val), format_count(total_),
config::color::RESET);
}
void Progress::print_cracked(const CrackResult& result) const {
if (!is_tty()) {
std::println("{}", result.plaintext);
return;
}
std::print("\033[3A\033[J");
std::println(" {}{} CRACKED {}{}{}",
config::color::GREEN, config::icon::CHECK,
std::string(30, '-'), config::color::RESET, "");
std::println(" {}Password: {}{}{}",
config::color::BOLD, config::color::GREEN,
result.plaintext, config::color::RESET);
std::println(" Hash: {}", result.hash);
std::println(" Algorithm: {}", result.algorithm);
std::println(" Time: {} {} {}",
format_time(result.elapsed_seconds),
config::box::VERTICAL,
format_speed(result.hashes_per_second));
}
void Progress::print_exhausted(std::string_view hash,
std::string_view algorithm) const {
if (!is_tty()) {
std::println("NOT FOUND");
return;
}
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - start_time).count();
auto tested_val = tested_.load(std::memory_order_relaxed);
std::print("\033[3A\033[J");
std::println(" {}{} EXHAUSTED {}{}{}",
config::color::RED, config::icon::CROSS,
std::string(30, '-'), config::color::RESET, "");
std::println(" Hash: {}", hash);
std::println(" Algorithm: {}", algorithm);
std::println(" Tested: {} candidates in {}",
format_count(tested_val), format_time(elapsed));
}

View File

@ -15,6 +15,15 @@ ThreadPool::ThreadPool(unsigned thread_count)
: thread_count_(thread_count > 0 ? thread_count
: std::thread::hardware_concurrency()) {}
void ThreadPool::run(WorkFn) {}
void ThreadPool::run(WorkFn work) {
std::vector<std::jthread> threads;
threads.reserve(thread_count_);
for (unsigned i = 0; i < thread_count_; ++i) {
threads.emplace_back([this, &work, i] {
work(i, thread_count_, state_);
});
}
}
SharedState& ThreadPool::state() { return state_; }

View File

@ -2,7 +2,44 @@
// test_engine.cpp
#include <gtest/gtest.h>
#include "src/core/Engine.hpp"
#include "src/hash/SHA256Hasher.hpp"
TEST(EngineTest, Stub) {
EXPECT_TRUE(true);
TEST(EngineTest, CracksSHA256WithDictionary) {
CrackConfig cfg;
cfg.target_hash =
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
cfg.wordlist_path = "tests/data/small_wordlist.txt";
cfg.thread_count = 2;
auto result = Engine::crack<SHA256Hasher, DictionaryAttack>(cfg);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->plaintext, "password");
}
TEST(EngineTest, ReturnsExhaustedWhenNotFound) {
CrackConfig cfg;
cfg.target_hash = std::string(64, 'f');
cfg.wordlist_path = "tests/data/small_wordlist.txt";
cfg.thread_count = 1;
auto result = Engine::crack<SHA256Hasher, DictionaryAttack>(cfg);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), CrackError::Exhausted);
}
TEST(EngineTest, CracksWithSalt) {
SHA256Hasher hasher;
auto salted_hash = hasher.hash("saltpassword");
CrackConfig cfg;
cfg.target_hash = salted_hash;
cfg.wordlist_path = "tests/data/small_wordlist.txt";
cfg.salt = "salt";
cfg.salt_position = "prepend";
cfg.thread_count = 1;
auto result = Engine::crack<SHA256Hasher, DictionaryAttack>(cfg);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->plaintext, "password");
}