From 0659873e129c18b182975902c74999c5e6a8d5bc Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Mon, 4 May 2026 21:49:29 -0500 Subject: [PATCH] Only use built-in zipfile module --- main.py | 13 ++---- pyproject.toml | 1 - tests/conftest.py | 30 +++++-------- tests/test_encrypted_zip.py | 87 +++++++++++++++++++++--------------- tests/testfile.zip | Bin 0 -> 358 bytes uv.lock | 44 ------------------ 6 files changed, 63 insertions(+), 112 deletions(-) create mode 100644 tests/testfile.zip diff --git a/main.py b/main.py index aed81b9..7b41e7f 100644 --- a/main.py +++ b/main.py @@ -72,13 +72,8 @@ class ZipFileIndexer(FileIndexer): self._password_cache: dict[str, bool] = {} def _open_zip(self): - """Open the ZIP file, using pyzipper if available for AES support.""" - try: - import pyzipper - - return pyzipper.AESZipFile(self.path, "r") - except ImportError: - return zipfile.ZipFile(self.path, "r") + """Open the ZIP file for reading.""" + return zipfile.ZipFile(self.path, "r") def _index(self) -> dict[str, str]: """Index all files in the zip file""" @@ -383,9 +378,7 @@ async def hash_page( image_click_url = _build_url(file_hash) # Create pause button to stop auto-refresh - pause_button = ( - f'' - ) + pause_button = f'' return _render_page( navigation_data, diff --git a/pyproject.toml b/pyproject.toml index b3ff56c..4ba98e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,5 @@ dev = [ "httpx>=0.28.1", "pytest>=9.0.3", "pytest-asyncio>=1.3.0", - "pyzipper>=0.3.6", "ruff>=0.15.5", ] diff --git a/tests/conftest.py b/tests/conftest.py index 3681389..3686aa9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ """Shared fixtures for the test suite.""" import argparse +import shutil import zipfile from collections.abc import Generator from pathlib import Path @@ -10,11 +11,6 @@ from httpx import ASGITransport, AsyncClient import main -try: - import pyzipper -except ImportError: - pyzipper = None - def _reset_state() -> None: """Reset all global state to defaults.""" @@ -131,26 +127,20 @@ async def client_zip(initialized_zip: None) -> Generator[AsyncClient]: @pytest.fixture def sample_encrypted_zip(tmp_path: Path) -> tuple[Path, dict[str, str], str]: - """Create a ZIP file with password-protected files. + """Copy the pre-existing testfile.zip to a temp location. Returns: Tuple of (zip_path, inner_files dict, password). inner_files maps logical names to internal paths. """ - if pyzipper is None: - pytest.skip("pyzipper not installed") - - zip_path = tmp_path / "encrypted.zip" - password = "secret123" - inner_files: dict[str, str] = {} - - with pyzipper.AESZipFile(zip_path, "w", encryption=pyzipper.WZ_AES) as zf: - zf.setpassword(password.encode()) - zf.writestr("protected.txt", "secret content") - inner_files["protected"] = "protected.txt" - zf.writestr("folder/secret.png", b"\x89PNG fake") - inner_files["secret_image"] = "folder/secret.png" - + src = Path(__file__).parent / "testfile.zip" + zip_path = tmp_path / "testfile.zip" + shutil.copy2(src, zip_path) + password = "password" + inner_files: dict[str, str] = { + "encrypted": "encrypted.txt", + "unencrypted": "unencrypted.txt", + } return zip_path, inner_files, password diff --git a/tests/test_encrypted_zip.py b/tests/test_encrypted_zip.py index a44f58d..8c9b81d 100644 --- a/tests/test_encrypted_zip.py +++ b/tests/test_encrypted_zip.py @@ -18,17 +18,17 @@ class TestLazyEncryptionDetection: """Encrypted files are detected as needing a password.""" zip_path, inner_files, _ = sample_encrypted_zip indexer = main.ZipFileIndexer(str(zip_path), salt="test") - protected_hash = indexer._hash_path(inner_files["protected"]) - assert indexer.is_file_encrypted(protected_hash) is True + encrypted_hash = indexer._hash_path(inner_files["encrypted"]) + assert indexer.is_file_encrypted(encrypted_hash) is True def test_detects_unencrypted_file( - self, sample_zip: dict[str, Path], tmp_path: Path + self, sample_encrypted_zip: tuple[Path, dict[str, str], str] ) -> None: """Unencrypted files are detected as not needing a password.""" - zip_path = tmp_path / "test_archive.zip" - indexer = main.ZipFileIndexer(str(zip_path), salt="test-salt") - top_hash = indexer._hash_path("top.txt") - assert indexer.is_file_encrypted(top_hash) is False + zip_path, inner_files, _ = sample_encrypted_zip + indexer = main.ZipFileIndexer(str(zip_path), salt="test") + unencrypted_hash = indexer._hash_path(inner_files["unencrypted"]) + assert indexer.is_file_encrypted(unencrypted_hash) is False def test_caches_detection_result( self, sample_encrypted_zip: tuple[Path, dict[str, str], str] @@ -36,14 +36,14 @@ class TestLazyEncryptionDetection: """Second call to is_file_encrypted uses the cache.""" zip_path, inner_files, _ = sample_encrypted_zip indexer = main.ZipFileIndexer(str(zip_path), salt="test") - protected_hash = indexer._hash_path(inner_files["protected"]) + encrypted_hash = indexer._hash_path(inner_files["encrypted"]) # First call populates the cache - assert indexer.is_file_encrypted(protected_hash) is True - assert "protected.txt" in indexer._password_cache + assert indexer.is_file_encrypted(encrypted_hash) is True + assert "encrypted.txt" in indexer._password_cache # Second call returns cached value - assert indexer.is_file_encrypted(protected_hash) is True + assert indexer.is_file_encrypted(encrypted_hash) is True def test_returns_false_for_unknown_hash( self, sample_encrypted_zip: tuple[Path, dict[str, str], str] @@ -59,11 +59,11 @@ class TestLazyEncryptionDetection: """Reading an encrypted file with the correct password succeeds.""" zip_path, inner_files, password = sample_encrypted_zip indexer = main.ZipFileIndexer(str(zip_path), salt="test") - protected_hash = indexer._hash_path(inner_files["protected"]) + encrypted_hash = indexer._hash_path(inner_files["encrypted"]) content = b"".join( - indexer.get_file_by_hash(protected_hash, password=password.encode()) + indexer.get_file_by_hash(encrypted_hash, password=password.encode()) ) - assert content == b"secret content" + assert content == b"You have read this file\n" def test_get_file_with_wrong_password_raises( self, sample_encrypted_zip: tuple[Path, dict[str, str], str] @@ -71,19 +71,19 @@ class TestLazyEncryptionDetection: """Reading an encrypted file with the wrong password raises.""" zip_path, inner_files, _ = sample_encrypted_zip indexer = main.ZipFileIndexer(str(zip_path), salt="test") - protected_hash = indexer._hash_path(inner_files["protected"]) + encrypted_hash = indexer._hash_path(inner_files["encrypted"]) with pytest.raises(RuntimeError): - list(indexer.get_file_by_hash(protected_hash, password=b"wrong")) + list(indexer.get_file_by_hash(encrypted_hash, password=b"wrong")) def test_get_unencrypted_file_without_password( - self, sample_zip: dict[str, Path], tmp_path: Path + self, sample_encrypted_zip: tuple[Path, dict[str, str], str] ) -> None: """Reading an unencrypted file works without a password.""" - zip_path = tmp_path / "test_archive.zip" - indexer = main.ZipFileIndexer(str(zip_path), salt="test-salt") - top_hash = indexer._hash_path("top.txt") - content = b"".join(indexer.get_file_by_hash(top_hash)) - assert content == b"top level content" + zip_path, inner_files, _ = sample_encrypted_zip + indexer = main.ZipFileIndexer(str(zip_path), salt="test") + unencrypted_hash = indexer._hash_path(inner_files["unencrypted"]) + content = b"".join(indexer.get_file_by_hash(unencrypted_hash)) + assert content == b"You have read this file\n" class TestEncryptedZipEndpoints: @@ -99,47 +99,47 @@ class TestEncryptedZipEndpoints: ) -> None: """Encrypted file returns 401 without auth on /api/{hash}/data.""" for hash_val, filepath in main.file_mapping.items(): - if filepath == "protected.txt": + if filepath == "encrypted.txt": response = await client_encrypted_zip.get(f"/api/{hash_val}/data") assert response.status_code == 401 break else: - pytest.fail("protected.txt not found in file_mapping") + pytest.fail("encrypted.txt not found in file_mapping") async def test_encrypted_file_requires_auth_on_hash_page( self, client_encrypted_zip: AsyncClient ) -> None: """Encrypted file returns 401 without auth on /{hash}.""" for hash_val, filepath in main.file_mapping.items(): - if filepath == "protected.txt": + if filepath == "encrypted.txt": response = await client_encrypted_zip.get(f"/{hash_val}") assert response.status_code == 401 break else: - pytest.fail("protected.txt not found in file_mapping") + pytest.fail("encrypted.txt not found in file_mapping") async def test_encrypted_file_succeeds_with_correct_auth( self, client_encrypted_zip: AsyncClient ) -> None: """Encrypted file returns 200 with correct auth on /api/{hash}/data.""" for hash_val, filepath in main.file_mapping.items(): - if filepath == "protected.txt": + if filepath == "encrypted.txt": response = await client_encrypted_zip.get( f"/api/{hash_val}/data", - headers={"Authorization": self._basic_auth("secret123")}, + headers={"Authorization": self._basic_auth("password")}, ) assert response.status_code == 200 - assert response.content == b"secret content" + assert response.content == b"You have read this file\n" break else: - pytest.fail("protected.txt not found in file_mapping") + pytest.fail("encrypted.txt not found in file_mapping") async def test_encrypted_file_wrong_password_returns_401( self, client_encrypted_zip: AsyncClient ) -> None: """Encrypted file returns 401 with wrong password.""" for hash_val, filepath in main.file_mapping.items(): - if filepath == "protected.txt": + if filepath == "encrypted.txt": response = await client_encrypted_zip.get( f"/api/{hash_val}/data", headers={"Authorization": self._basic_auth("wrong")}, @@ -147,37 +147,50 @@ class TestEncryptedZipEndpoints: assert response.status_code == 401 break else: - pytest.fail("protected.txt not found in file_mapping") + pytest.fail("encrypted.txt not found in file_mapping") async def test_www_authenticate_header_present( self, client_encrypted_zip: AsyncClient ) -> None: """401 response includes WWW-Authenticate header.""" for hash_val, filepath in main.file_mapping.items(): - if filepath == "protected.txt": + if filepath == "encrypted.txt": response = await client_encrypted_zip.get(f"/api/{hash_val}/data") assert response.status_code == 401 assert "www-authenticate" in response.headers assert "Basic" in response.headers["www-authenticate"] break else: - pytest.fail("protected.txt not found in file_mapping") + pytest.fail("encrypted.txt not found in file_mapping") async def test_encrypted_hash_page_succeeds_with_auth( self, client_encrypted_zip: AsyncClient ) -> None: """Encrypted file hash page returns 200 with correct auth.""" for hash_val, filepath in main.file_mapping.items(): - if filepath == "protected.txt": + if filepath == "encrypted.txt": response = await client_encrypted_zip.get( f"/{hash_val}", - headers={"Authorization": self._basic_auth("secret123")}, + headers={"Authorization": self._basic_auth("password")}, ) assert response.status_code == 200 assert "text/html" in response.headers["content-type"] break else: - pytest.fail("protected.txt not found in file_mapping") + pytest.fail("encrypted.txt not found in file_mapping") + + async def test_unencrypted_file_no_auth_required( + self, client_encrypted_zip: AsyncClient + ) -> None: + """Unencrypted files in the same ZIP work without any auth.""" + for hash_val, filepath in main.file_mapping.items(): + if filepath == "unencrypted.txt": + response = await client_encrypted_zip.get(f"/api/{hash_val}/data") + assert response.status_code == 200 + assert response.content == b"You have read this file\n" + break + else: + pytest.fail("unencrypted.txt not found in file_mapping") async def test_unencrypted_zip_no_auth_required( self, client_zip: AsyncClient diff --git a/tests/testfile.zip b/tests/testfile.zip new file mode 100644 index 0000000000000000000000000000000000000000..666a01a3670cb59b93aeed993ad7336784366f8c GIT binary patch literal 358 zcmWIWW@Zs#0D)~?OJY&zpP3+BTEWf0$nt`j zfdNeLzxJIfF%LCM&V}^6)Lx`_VEVU0-V@n