diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index 11919c5..9112363 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -1195,17 +1195,29 @@ def wordlist_shard(ctx: Any) -> None: def wordlist_optimize(ctx: Any) -> None: """Prompt for input wordlists and output directory, then optimize.""" raw = ctx.select_file_with_autocomplete( - "\n[*] Enter input wordlist paths (comma-separated)", + "\n[*] Enter input wordlist paths (comma-separated files or directories)", base_dir=ctx.hcatWordlists, ).strip() - inputs = [p.strip() for p in raw.split(",") if p.strip()] - if not inputs: + raw_entries = [p.strip() for p in raw.split(",") if p.strip()] + if not raw_entries: print("[!] No input wordlists provided.") return - missing = [p for p in inputs if not os.path.isfile(p)] - if missing: - print("[!] Files not found:") - for p in missing: + inputs: list[str] = [] + not_found: list[str] = [] + for entry in raw_entries: + if os.path.isfile(entry): + inputs.append(entry) + elif os.path.isdir(entry): + files = [os.path.join(entry, f) for f in ctx.list_wordlist_files(entry)] + if not files: + print(f"[!] No wordlist files found in: {entry}") + return + inputs.extend(files) + else: + not_found.append(entry) + if not_found: + print("[!] Not found (not a file or directory):") + for p in not_found: print(f" {p}") return outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip() diff --git a/hate_crack/main.py b/hate_crack/main.py index a4d8681..e06e7ca 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -1079,12 +1079,17 @@ def select_file_with_autocomplete( except IndexError: return None + def display_matches(substitution, matches, longest_match_length): + print() + for match in matches: + print(f" {match}") + readline.redisplay() + # Configure readline for tab completion readline.set_completer_delims(" \t\n;") - # Disable the "Display all X possibilities?" prompt try: - readline.parse_and_bind("set completion-query-items -1") - except Exception: + readline.set_completion_display_matches_hook(display_matches) + except AttributeError: pass try: readline.parse_and_bind("tab: complete") @@ -4892,7 +4897,9 @@ def main(): ("2", "Download wordlists from Weakpass"), ("3", "Download wordlists from Hashmob.net"), ("4", "Download rules from Hashmob.net"), - ("5", "Exit"), + ("5", "Wordlist Tools"), + ("6", "Rule File Tools"), + ("7", "Exit"), ] menu_loop = True while menu_loop: @@ -4930,6 +4937,10 @@ def main(): sys.exit(0) # Otherwise continue the menu loop elif choice == "5": + wordlist_tools_submenu() + elif choice == "6": + rule_tools_submenu() + elif choice == "7": sys.exit(0) else: if ( diff --git a/tests/test_wordlist_tools.py b/tests/test_wordlist_tools.py index 52710f7..86a7508 100644 --- a/tests/test_wordlist_tools.py +++ b/tests/test_wordlist_tools.py @@ -330,7 +330,10 @@ class TestWordlistOptimize: f"{wl_a},{wl_b}", outdir, ] - with patch("hate_crack.attacks.os.path.isfile", return_value=True): + with ( + patch("hate_crack.attacks.os.path.isfile", return_value=True), + patch("hate_crack.attacks.os.path.isdir", return_value=False), + ): wordlist_optimize(ctx) ctx.wordlist_optimize.assert_called_once_with( [str(wl_a), str(wl_b)], outdir @@ -338,6 +341,35 @@ class TestWordlistOptimize: out = capsys.readouterr().out assert outdir in out + def test_directory_expansion(self, tmp_path, capsys): + ctx = _make_ctx() + wl_dir = str(tmp_path / "wls") + outdir = str(tmp_path / "out") + ctx.select_file_with_autocomplete.side_effect = [wl_dir, outdir] + ctx.list_wordlist_files.return_value = ["a.txt", "b.txt"] + with ( + patch("hate_crack.attacks.os.path.isfile", return_value=False), + patch("hate_crack.attacks.os.path.isdir", return_value=True), + ): + wordlist_optimize(ctx) + ctx.wordlist_optimize.assert_called_once_with( + [os.path.join(wl_dir, "a.txt"), os.path.join(wl_dir, "b.txt")], outdir + ) + + def test_empty_directory_rejection(self, tmp_path, capsys): + ctx = _make_ctx() + wl_dir = str(tmp_path / "wls") + ctx.select_file_with_autocomplete.return_value = wl_dir + ctx.list_wordlist_files.return_value = [] + with ( + patch("hate_crack.attacks.os.path.isfile", return_value=False), + patch("hate_crack.attacks.os.path.isdir", return_value=True), + ): + wordlist_optimize(ctx) + out = capsys.readouterr().out + assert "No wordlist files found" in out + ctx.wordlist_optimize.assert_not_called() + def test_empty_input_rejection(self, capsys): ctx = _make_ctx() ctx.select_file_with_autocomplete.return_value = "," @@ -359,10 +391,13 @@ class TestWordlistOptimize: existing = tmp_path / "a.txt" existing.write_text("word\n") ctx.select_file_with_autocomplete.return_value = f"{existing},/nonexistent/missing.txt" - with patch("hate_crack.attacks.os.path.isfile", side_effect=lambda p: p == str(existing)): + with ( + patch("hate_crack.attacks.os.path.isfile", side_effect=lambda p: p == str(existing)), + patch("hate_crack.attacks.os.path.isdir", return_value=False), + ): wordlist_optimize(ctx) out = capsys.readouterr().out - assert "Files not found" in out + assert "Not found" in out ctx.wordlist_optimize.assert_not_called() def test_empty_outdir_rejection(self, tmp_path, capsys): @@ -370,7 +405,10 @@ class TestWordlistOptimize: wl = tmp_path / "a.txt" wl.write_text("word\n") ctx.select_file_with_autocomplete.side_effect = [str(wl), ""] - with patch("hate_crack.attacks.os.path.isfile", return_value=True): + with ( + patch("hate_crack.attacks.os.path.isfile", return_value=True), + patch("hate_crack.attacks.os.path.isdir", return_value=False), + ): wordlist_optimize(ctx) out = capsys.readouterr().out assert "Output directory cannot be empty" in out @@ -383,7 +421,10 @@ class TestWordlistOptimize: wl.write_text("word\n") outdir = str(tmp_path / "out") ctx.select_file_with_autocomplete.side_effect = [str(wl), outdir] - with patch("hate_crack.attacks.os.path.isfile", return_value=True): + with ( + patch("hate_crack.attacks.os.path.isfile", return_value=True), + patch("hate_crack.attacks.os.path.isdir", return_value=False), + ): wordlist_optimize(ctx) out = capsys.readouterr().out assert "Optimization failed" in out