diff --git a/booru_viewer/core/__init__.py b/booru_viewer/core/__init__.py index e69de29..ee6eb2d 100644 --- a/booru_viewer/core/__init__.py +++ b/booru_viewer/core/__init__.py @@ -0,0 +1,19 @@ +"""booru_viewer.core package — pure-Python data + I/O layer (no Qt). + +Side effect on import: install the project-wide PIL decompression-bomb +cap. PIL's default warns silently above ~89M pixels; we want a hard +fail above 256M pixels so DecompressionBombError can be caught and +treated as a download failure. + +Setting it here (rather than as a side effect of importing +``core.cache``) means any code path that touches PIL via any +``booru_viewer.core.*`` submodule gets the cap installed first — +``core.images`` no longer depends on ``core.cache`` having been +imported in the right order. Audit finding #8. +""" + +from PIL import Image as _PILImage + +_PILImage.MAX_IMAGE_PIXELS = 256 * 1024 * 1024 + +del _PILImage diff --git a/tests/core/test_pil_safety.py b/tests/core/test_pil_safety.py new file mode 100644 index 0000000..6ac135c --- /dev/null +++ b/tests/core/test_pil_safety.py @@ -0,0 +1,57 @@ +"""Tests for the project-wide PIL decompression-bomb cap (audit #8). + +The cap lives in `booru_viewer/core/__init__.py` so any import of +any `booru_viewer.core.*` submodule installs it first — independent +of whether `core.cache` is on the import path. Both checks are run +in a fresh subprocess so the assertion isn't masked by some other +test's previous import. +""" + +from __future__ import annotations + +import subprocess +import sys + +EXPECTED = 256 * 1024 * 1024 + + +def _run(code: str) -> str: + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +def test_core_package_import_installs_cap(): + """Importing the core package alone must set MAX_IMAGE_PIXELS.""" + out = _run( + "import booru_viewer.core; " + "from PIL import Image; " + "print(Image.MAX_IMAGE_PIXELS)" + ) + assert int(out) == EXPECTED + + +def test_core_images_import_installs_cap(): + """The original audit concern: importing core.images without first + importing core.cache must still set the cap.""" + out = _run( + "from booru_viewer.core import images; " + "from PIL import Image; " + "print(Image.MAX_IMAGE_PIXELS)" + ) + assert int(out) == EXPECTED + + +def test_core_cache_import_still_installs_cap(): + """Regression: the old code path (importing cache first) must keep + working after the move.""" + out = _run( + "from booru_viewer.core import cache; " + "from PIL import Image; " + "print(Image.MAX_IMAGE_PIXELS)" + ) + assert int(out) == EXPECTED