fix: separate hcatPath (hashcat dir) from hate_path (asset dir)

hcatPath now exclusively points to the hashcat install directory and is
auto-discovered from PATH when not configured. hate_path is resolved
from the package directory (installed) or repo root (development) with
no auto-discovery. Extracted vendor-assets/clean-vendor Makefile targets
to deduplicate the install logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-02-12 20:23:12 -05:00
co-authored by Claude Opus 4.6
parent c260787985
commit 91906e3313
6 changed files with 54 additions and 103 deletions
+17 -19
View File
@@ -1,5 +1,5 @@
.DEFAULT_GOAL := submodules
.PHONY: install reinstall dev-install dev-reinstall clean hashcat-utils submodules submodules-pre test coverage lint check ruff mypy
.PHONY: install reinstall dev-install dev-reinstall clean hashcat-utils submodules submodules-pre vendor-assets clean-vendor test coverage lint check ruff mypy
hashcat-utils: submodules
$(MAKE) -C hashcat-utils
@@ -28,40 +28,38 @@ submodules-pre:
@# Patch hashcat-utils/src/Makefile so these new expanders are compiled by default.
@bases="hashcat-utils hate_crack/hashcat-utils"; for base in $$bases; do src="$$base/src/expander.c"; test -f "$$src" || continue; for i in $$(seq 8 36); do dst="$$base/src/expander$$i.c"; if [ ! -f "$$dst" ]; then cp "$$src" "$$dst"; perl -pi -e "s/#define LEN_MAX 7/#define LEN_MAX $$i/g" "$$dst"; fi; done; mk="$$base/src/Makefile"; test -f "$$mk" || continue; exp_bins=""; exp_exes=""; for i in $$(seq 8 36); do exp_bins="$$exp_bins expander$$i.bin"; exp_exes="$$exp_exes expander$$i.exe"; done; EXP_BINS="$$exp_bins" perl -pi -e 'if(/^native:/ && index($$_, "expander8.bin") < 0){chomp; $$_ .= "$$ENV{EXP_BINS}"; $$_ .= "\n";}' "$$mk"; EXP_EXES="$$exp_exes" perl -pi -e 'if(/^windows:/ && index($$_, "expander8.exe") < 0){chomp; $$_ .= "$$ENV{EXP_EXES}"; $$_ .= "\n";}' "$$mk"; perl -0777 -pi -e 's/\n# Auto-added by hate_crack \\(submodules-pre\\)\n.*\z/\n/s' "$$mk"; printf '%s\n' '' '# Auto-added by hate_crack (submodules-pre)' 'expander%.bin: src/expander%.c' >> "$$mk"; printf '\t%s\n' '$${CC_NATIVE} $${CFLAGS_NATIVE} $${LDFLAGS_NATIVE} -o bin/$$@ $$<' >> "$$mk"; printf '%s\n' '' 'expander%.exe: src/expander%.c' >> "$$mk"; printf '\t%s\n' '$${CC_WINDOWS} $${CFLAGS_WINDOWS} -o bin/$$@ $$<' >> "$$mk"; done
install:
@echo "Detecting OS and installing dependencies..."
vendor-assets:
@if [ ! -f princeprocessor/pp64.bin ] && [ ! -f princeprocessor/pp64.app ] && [ ! -f princeprocessor/pp64.exe ]; then \
echo "princeprocessor binaries are missing; please ensure the princeprocessor directory is present."; \
exit 1; \
fi
@echo "Syncing assets into package for uv tool install..."
@rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor
@cp -R hashcat-utils hate_crack/
@cp -R princeprocessor hate_crack/
@rm -rf hate_crack/hashcat-utils/.git hate_crack/princeprocessor/.git
clean-vendor:
@echo "Cleaning up vendored assets from working tree..."
@rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor
install: submodules vendor-assets
@echo "Detecting OS and installing dependencies..."
@if [ "$(shell uname)" = "Darwin" ]; then \
echo "Detected macOS"; \
command -v brew >/dev/null 2>&1 || { echo >&2 "Homebrew not found. Please install Homebrew first: https://brew.sh/"; exit 1; }; \
brew install p7zip transmission-cli; \
echo "Syncing assets into package for uv tool install..."; \
rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor; \
cp -R hashcat-utils hate_crack/; \
cp -R princeprocessor hate_crack/; \
rm -rf hate_crack/hashcat-utils/.git hate_crack/princeprocessor/.git; \
uv tool install .; \
echo "Cleaning up vendored assets from working tree..."; \
rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor; \
elif [ -f /etc/debian_version ]; then \
echo "Detected Debian/Ubuntu"; \
sudo apt-get update; \
sudo apt-get install -y p7zip-full transmission-cli; \
echo "Syncing assets into package for uv tool install..."; \
rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor; \
cp -R hashcat-utils hate_crack/; \
cp -R princeprocessor hate_crack/; \
rm -rf hate_crack/hashcat-utils/.git hate_crack/princeprocessor/.git; \
uv tool install .; \
echo "Cleaning up vendored assets from working tree..."; \
rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor; \
else \
echo "Unsupported OS. Please install dependencies manually."; \
$(MAKE) clean-vendor; \
exit 1; \
fi
@uv tool install .
@$(MAKE) clean-vendor
reinstall: uninstall install
+6 -13
View File
@@ -139,7 +139,7 @@ cd /path/to/hate_crack
hate_crack <hash_file> <hash_type>
```
To keep `uv tool install .` happy, run the tool from the hate_crack checkout directory or one of the auto-discovered locations below.
Run `make install` to install the tool with all assets bundled into the package.
**Note:** The `hcatPath` in `config.json` is for the hashcat binary location (optional if hashcat is in PATH), not for hate_crack assets.
@@ -168,26 +168,19 @@ Error: Build directory /opt/hashcat/hashcat-utils does not exist.
Expected to find expander at /opt/hashcat/hashcat-utils/bin/expander.
```
This means the tool cannot find the hate_crack repository assets. The `hashcat-utils` and `princeprocessor` directories are part of the **hate_crack repository**, not the hashcat installation.
This means the hate_crack assets were not bundled into the installed package.
**Understanding the paths:**
- `hcatPath` in config.json → points to **hashcat binary location** (optional, can be in PATH)
- `hashcat-utils/` and `princeprocessor/`located in the **hate_crack repository directory**
The tool automatically searches for hate_crack assets in these locations:
1. The directory that contains the hate_crack checkout (and includes `config.json`, `hashcat-utils/`, and `princeprocessor/`)
2. Current working directory and parent directory
3. `~/hate_crack`, `~/hate-crack`, or `~/.hate_crack`
- `hashcat-utils/` and `princeprocessor/`bundled into the package by `make install`
**Solution:**
Run `hate_crack` from within the repository directory:
Reinstall using the Makefile, which vendors the assets into the package:
```bash
cd /opt/hate_crack # or wherever you cloned the repository
hate_crack <hash_file> <hash_type>
cd /path/to/hate_crack # the repository checkout
make install
```
If the assets live elsewhere, update `"hcatPath"` in `config.json` to point to the directory that contains `hashcat-utils` and `princeprocessor`.
**Example config.json:**
```json
{
+5 -13
View File
@@ -3,26 +3,18 @@
## Overview
The test suite uses mocked API responses and local fixtures so it can run without external services (Hashview, Hashmob, Weakpass). Most tests are fast and run entirely offline. Live network checks and system dependency checks are now opt-in via environment variables.
## Important: Asset Location (hcatPath)
## Important: Asset Location
The application requires access to `hashcat-utils` and `princeprocessor` as subdirectories. When running tests or the tool outside the repository directory, configure the asset location in `config.json` by setting `hcatPath`:
The application requires `hashcat-utils` and `princeprocessor` as subdirectories of the package. When installed via `make install`, these are vendored into the package automatically.
```json
{
"hcatPath": "/path/to/hate_crack",
...
}
```
Then run tests or the tool normally:
For development, run tests from the repository directory:
```bash
cd /path/to/hate_crack
uv run pytest -v
# or
hate_crack <hash_file> <hash_type>
```
If `hcatPath` is not set (empty string), the application will search the current directory and parent directory for these assets.
**Note:** `hcatPath` in `config.json` is for the **hashcat binary location** (optional if hashcat is in PATH), not for hate_crack assets.
## Changes Made
+24 -55
View File
@@ -111,34 +111,6 @@ def _resolve_config_destination():
return os.getcwd()
def _resolve_hate_path(package_path, config_dict=None):
# Try to use hcatPath from config.json if it's set and contains assets
if config_dict and config_dict.get("hcatPath"):
assets_path = config_dict.get("hcatPath")
if _has_hate_crack_assets(assets_path):
return assets_path
# Check common locations for the repo
for candidate in _candidate_roots():
if _has_hate_crack_assets(candidate):
return candidate
# When installed as a tool, assets should be defined in config.json hcatPath
# If not set, default to package path (which may not have assets)
if _has_hate_crack_assets(package_path):
return package_path
# Last resort: return package_path, but this likely means assets are missing
if not config_dict or not config_dict.get("hcatPath"):
print(
"\nWarning: Could not find hate_crack assets (hashcat-utils, princeprocessor)."
)
print("Set 'hcatPath' in config.json to the installation directory:")
print(' "hcatPath": "/path/to/hate_crack"')
print("Or run from the repository directory where these assets are located.\n")
return package_path
def _ensure_hashfile_in_cwd(hashfile_path):
"""Ensure hashfile path points to cwd to keep output files in execution dir."""
@@ -167,13 +139,22 @@ def _ensure_hashfile_in_cwd(hashfile_path):
return hashfile_path
# First get a temporary path to load config
_initial_package_path = os.path.dirname(os.path.realpath(__file__))
# hate_path is where hate_crack assets live (hashcat-utils, princeprocessor, etc.)
# When installed via `make install`, assets are vendored into the package directory.
# During development, assets live in the repo root (parent of the package directory).
_package_path = os.path.dirname(os.path.realpath(__file__))
_repo_root = os.path.dirname(_package_path)
if os.path.isdir(os.path.join(_package_path, "hashcat-utils")):
hate_path = _package_path
elif os.path.isdir(os.path.join(_repo_root, "hashcat-utils")):
hate_path = _repo_root
else:
hate_path = _package_path
_config_path = _resolve_config_path()
if not _config_path:
print("Initializing config.json from config.json.example")
src_config = os.path.abspath(
os.path.join(_initial_package_path, "config.json.example")
os.path.join(_package_path, "config.json.example")
)
config_dir = _resolve_config_destination()
dst_config = os.path.abspath(os.path.join(config_dir, "config.json"))
@@ -188,22 +169,10 @@ with open(_config_path) as config:
config_dir = os.path.dirname(_config_path)
defaults_path = os.path.join(config_dir, "config.json.example")
if not os.path.isfile(defaults_path):
defaults_path = os.path.join(_initial_package_path, "config.json.example")
defaults_path = os.path.join(_package_path, "config.json.example")
with open(defaults_path) as defaults:
default_config = json.load(defaults)
# Now resolve hate_path using config
hate_path = _resolve_hate_path(_initial_package_path, config_parser)
# If hate_path differs from initial path, reload config from the new location
if hate_path != _initial_package_path:
if os.path.isfile(hate_path + "/config.json"):
with open(hate_path + "/config.json") as config:
config_parser = json.load(config)
if os.path.isfile(hate_path + "/config.json.example"):
with open(hate_path + "/config.json.example") as defaults:
default_config = json.load(defaults)
try:
hashview_url = config_parser["hashview_url"]
except KeyError as e:
@@ -241,14 +210,11 @@ def ensure_binary(binary_path, build_dir=None, name=None):
"\nThe hate_crack assets (hashcat-utils, princeprocessor) could not be found."
)
print(
"These are part of the hate_crack repository, not hashcat installation."
"\nRun 'make install' from the repository directory to install with assets:"
)
print("\nPlease run hate_crack from the repository directory:")
print(" cd /path/to/hate_crack && hate_crack <hash_file> <hash_type>")
print(
'\nOr set "hcatPath" in config.json to the hate_crack directory that contains hashcat-utils and princeprocessor:'
" cd /path/to/hate_crack && make install"
)
print(' "hcatPath": "/path/to/hate_crack"')
quit(1)
# Binary missing - need to build
@@ -265,16 +231,20 @@ def ensure_binary(binary_path, build_dir=None, name=None):
return binary_path
# NOTE: hcatPath is for hashcat binary location, NOT for hate_crack assets
# If empty in config, we fall back to hate_path as a convenience
# But hashcat-utils and princeprocessor should ALWAYS use hate_path
hcatPath = config_parser.get("hcatPath", "") or hate_path
# NOTE: hcatPath is the hashcat install directory, NOT for hate_crack assets.
# hashcat-utils and princeprocessor should ALWAYS use hate_path.
hcatPath = config_parser.get("hcatPath", "")
hcatBin = config_parser["hcatBin"]
# If hcatBin is not absolute and hcatPath is set, construct full path from hcatPath + hcatBin
if not os.path.isabs(hcatBin) and hcatPath:
_candidate = os.path.join(hcatPath, hcatBin)
if os.path.isfile(_candidate):
hcatBin = _candidate
# When hcatPath is not configured, discover it from the hashcat binary in PATH
if not hcatPath:
_which = shutil.which(hcatBin)
if _which:
hcatPath = os.path.dirname(os.path.realpath(_which))
hcatTuning = config_parser["hcatTuning"]
hcatWordlists = config_parser["hcatWordlists"]
hcatOptimizedWordlists = config_parser["hcatOptimizedWordlists"]
@@ -621,8 +591,7 @@ hcatPrinceBaseList = _normalize_wordlist_setting(hcatPrinceBaseList, wordlists_d
if not SKIP_INIT:
# Verify hashcat binary is available
# hcatPath is for assets (hashcat-utils, princeprocessor), not hashcat binary location
# hcatBin should be in PATH or be an absolute path
# hcatBin should be in PATH or be an absolute path (resolved from hcatPath + hcatBin if configured)
try:
if os.path.isabs(hcatBin):
if not os.path.isfile(hcatBin):
+1 -2
View File
@@ -28,8 +28,7 @@ def test_hashcat_utils_uses_hate_path_not_hcat_path(tmp_path, monkeypatch):
assert os.path.isdir(main.hate_path)
assert os.path.isdir(os.path.join(main.hate_path, "hashcat-utils"))
# The hcatPath might be different (fallback to hate_path if empty)
# But utilities should ALWAYS use hate_path
# hcatPath is the hashcat install directory, not the hate_crack package directory
assert main.hcatPath is not None
# Key assertion: even if hcatPath != hate_path,
+1 -1
View File
@@ -28,7 +28,7 @@ def test_ensure_binary_error_message(monkeypatch, capsys):
captured = capsys.readouterr()
assert "Build directory" in captured.out or "does not exist" in captured.out
assert "hate_crack" in captured.out.lower() # Should mention hate_crack assets
assert "hcatPath" in captured.out or "repository" in captured.out
assert "make install" in captured.out
def test_ensure_binary_with_existing_binary():