diff --git a/hate_crack/api.py b/hate_crack/api.py index db962a3..f80b1d4 100644 --- a/hate_crack/api.py +++ b/hate_crack/api.py @@ -11,6 +11,7 @@ from typing import Callable, Optional, Tuple import requests # type: ignore[import-untyped] from bs4 import BeautifulSoup +from hate_crack.cli import orig_cwd from hate_crack.formatting import print_multicolumn_list _TORRENT_CLEANUP_REGISTERED = False @@ -841,8 +842,10 @@ class HashviewAPI: resp.raise_for_status() if output_file is None: output_file = f"left_{customer_id}_{hashfile_id}.txt" - # Convert to absolute path to ensure file is preserved if CWD changes - output_file = os.path.abspath(output_file) + # Resolve relative paths against the user's original CWD, not the + # install directory that ``uv run --directory`` may have switched to. + if not os.path.isabs(output_file): + output_file = os.path.join(orig_cwd(), output_file) output_abs = output_file total = int(resp.headers.get("content-length", 0)) downloaded = 0 @@ -869,7 +872,7 @@ class HashviewAPI: # Try to download found file and process with hashcat combined_count = 0 combined_file = None - out_dir = os.path.dirname(output_abs) or os.getcwd() + out_dir = os.path.dirname(output_abs) or orig_cwd() found_file = os.path.join(out_dir, f"found_{customer_id}_{hashfile_id}.txt") try: diff --git a/hate_crack/cli.py b/hate_crack/cli.py index 1922351..4cfcd67 100644 --- a/hate_crack/cli.py +++ b/hate_crack/cli.py @@ -4,6 +4,17 @@ import sys from typing import Optional +def orig_cwd() -> str: + """Return the caller's original working directory. + + When invoked via the bash shim (``uv run --directory``), the process CWD + is the install directory, not where the user ran the command. + ``HATE_CRACK_ORIG_CWD`` preserves the real CWD so that output files + (e.g. ``.out``, ``.nt``) are created next to the hashfile the user specified. + """ + return os.environ.get("HATE_CRACK_ORIG_CWD") or os.getcwd() + + def resolve_path(value: Optional[str]) -> Optional[str]: """Expand user and return an absolute path, or None. diff --git a/hate_crack/main.py b/hate_crack/main.py index ffe5ae1..4e8110e 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -69,6 +69,7 @@ from hate_crack.api import ( # noqa: E402 extract_with_7z, ) from hate_crack.cli import ( # noqa: E402 + orig_cwd, resolve_path, setup_logging, ) @@ -198,10 +199,7 @@ def _ensure_hashfile_in_cwd(hashfile_path): """Ensure hashfile path points to cwd to keep output files in execution dir.""" if not hashfile_path: return hashfile_path - try: - cwd = os.getcwd() - except Exception: - return hashfile_path + cwd = orig_cwd() if not os.path.isabs(hashfile_path): return hashfile_path if os.path.dirname(hashfile_path) == cwd: @@ -358,7 +356,7 @@ if "hcatPotfilePath" not in config_parser: if os.path.isfile(_default_pot) or os.path.isdir(os.path.dirname(_default_pot)): hcatPotfilePath = _default_pot else: - hcatPotfilePath = os.path.join(os.getcwd(), "hashcat.potfile") + hcatPotfilePath = os.path.join(orig_cwd(), "hashcat.potfile") else: _raw_pot = (config_parser.get("hcatPotfilePath") or "").strip() if _raw_pot == "": diff --git a/tests/test_hashview.py b/tests/test_hashview.py index 993cc43..b93a3dd 100644 --- a/tests/test_hashview.py +++ b/tests/test_hashview.py @@ -774,14 +774,8 @@ class TestHashviewAPI: content = f.read() assert content == b"hash1\nhash2\n" - def test_hashfile_orig_path_preservation(self, tmp_path): + def test_hashfile_orig_path_preservation(self, tmp_path, monkeypatch): """Test that original hashfile path is preserved before _ensure_hashfile_in_cwd""" - import sys - import os - - sys.path.insert( - 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - ) from hate_crack.main import _ensure_hashfile_in_cwd # Create a test hashfile in a different directory @@ -792,27 +786,22 @@ class TestHashviewAPI: original_path = str(test_file) - # Save current directory - orig_cwd = os.getcwd() - try: - # Change to tmp_path - os.chdir(str(tmp_path)) + # Set HATE_CRACK_ORIG_CWD so _ensure_hashfile_in_cwd targets tmp_path + monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(tmp_path)) - # Call _ensure_hashfile_in_cwd - result_path = _ensure_hashfile_in_cwd(original_path) + # Call _ensure_hashfile_in_cwd + result_path = _ensure_hashfile_in_cwd(original_path) - # The result should be different from original (in cwd now) - # But original_path should still exist and be unchanged - assert os.path.exists(original_path), "Original file should still exist" - assert os.path.exists(result_path), "Result file should exist" + # The result should be different from original (in cwd now) + # But original_path should still exist and be unchanged + assert os.path.exists(original_path), "Original file should still exist" + assert os.path.exists(result_path), "Result file should exist" - # If they're different, result should be in cwd - if result_path != original_path: - assert os.path.dirname(result_path) == str(tmp_path), ( - "Result should be in cwd" - ) - finally: - os.chdir(orig_cwd) + # If they're different, result should be in cwd + if result_path != original_path: + assert os.path.dirname(result_path) == str(tmp_path), ( + "Result should be in cwd" + ) if __name__ == "__main__": diff --git a/tests/test_main_utils.py b/tests/test_main_utils.py index f9880fe..2569f44 100644 --- a/tests/test_main_utils.py +++ b/tests/test_main_utils.py @@ -93,14 +93,14 @@ class TestEnsureHashfileInCwd: result = main_module._ensure_hashfile_in_cwd("relative/path.txt") assert result == "relative/path.txt" - def test_already_in_cwd(self, main_module, tmp_path): + def test_already_in_cwd(self, main_module, tmp_path, monkeypatch): target = tmp_path / "hashfile.txt" target.write_text("hashes") - with patch("os.getcwd", return_value=str(tmp_path)): - result = main_module._ensure_hashfile_in_cwd(str(target)) + monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(tmp_path)) + result = main_module._ensure_hashfile_in_cwd(str(target)) assert result == str(target) - def test_different_dir_existing_file_in_cwd(self, main_module, tmp_path): + def test_different_dir_existing_file_in_cwd(self, main_module, tmp_path, monkeypatch): # File exists in a different directory other_dir = tmp_path / "other" other_dir.mkdir() @@ -113,11 +113,11 @@ class TestEnsureHashfileInCwd: cwd_copy = cwd_dir / "hashes.txt" cwd_copy.write_text("cwd version") - with patch("os.getcwd", return_value=str(cwd_dir)): - result = main_module._ensure_hashfile_in_cwd(str(source_file)) + monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(cwd_dir)) + result = main_module._ensure_hashfile_in_cwd(str(source_file)) assert result == str(cwd_copy) - def test_different_dir_creates_symlink(self, main_module, tmp_path): + def test_different_dir_creates_symlink(self, main_module, tmp_path, monkeypatch): # Source file in a different directory, nothing in cwd other_dir = tmp_path / "other" other_dir.mkdir() @@ -127,13 +127,34 @@ class TestEnsureHashfileInCwd: cwd_dir = tmp_path / "cwd" cwd_dir.mkdir() - with patch("os.getcwd", return_value=str(cwd_dir)): - result = main_module._ensure_hashfile_in_cwd(str(source_file)) + monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(cwd_dir)) + result = main_module._ensure_hashfile_in_cwd(str(source_file)) expected = str(cwd_dir / "hashes.txt") assert result == expected assert os.path.exists(expected) + def test_uses_orig_cwd_not_process_cwd(self, main_module, tmp_path, monkeypatch): + """Output files go to HATE_CRACK_ORIG_CWD, not the process CWD.""" + # Simulate: process CWD is the install dir, orig CWD is the user's dir + install_dir = tmp_path / "install" + install_dir.mkdir() + user_dir = tmp_path / "user" + user_dir.mkdir() + other_dir = tmp_path / "other" + other_dir.mkdir() + source_file = other_dir / "hashes.txt" + source_file.write_text("hashes") + + monkeypatch.chdir(install_dir) + monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(user_dir)) + result = main_module._ensure_hashfile_in_cwd(str(source_file)) + + # Should land in user_dir, NOT install_dir + assert result == str(user_dir / "hashes.txt") + assert os.path.exists(user_dir / "hashes.txt") + assert not os.path.exists(install_dir / "hashes.txt") + class TestRunHashcatShow: def _make_mock_result(self, stdout_bytes): diff --git a/tests/test_utils.py b/tests/test_utils.py index af553f3..df2f12f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,6 +8,18 @@ from hate_crack import cli from hate_crack import formatting +def test_orig_cwd_uses_env_var(monkeypatch, tmp_path): + """orig_cwd() returns HATE_CRACK_ORIG_CWD when set.""" + monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(tmp_path)) + assert cli.orig_cwd() == str(tmp_path) + + +def test_orig_cwd_falls_back_to_getcwd(monkeypatch): + """orig_cwd() falls back to os.getcwd() when env var is unset.""" + monkeypatch.delenv("HATE_CRACK_ORIG_CWD", raising=False) + assert cli.orig_cwd() == os.getcwd() + + def test_resolve_path_none_and_expand(): assert cli.resolve_path("") is None resolved = cli.resolve_path("~")