From 5db9b4c5385e2a14f487c3adad77c06aef6e5f28 Mon Sep 17 00:00:00 2001 From: DysektAI <33468668+DysektAI@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:03:04 -0400 Subject: [PATCH 1/2] fix(security): upgrade Pillow and restore validated dependency installs --- .github/workflows/build-release.yml | 39 ++++++- README.md | 8 +- pyproject.toml | 2 +- requirements.txt | 172 ++++++++++++++-------------- scripts/test_build_data.py | 109 ++++++++++++++++++ 5 files changed, 238 insertions(+), 92 deletions(-) create mode 100644 scripts/test_build_data.py diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 79d7931..c932610 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -8,6 +8,12 @@ on: - ".github/workflows/build-release.yml" - "requirements*.txt" - "scripts/**" + pull_request: + paths: + - ".github/workflows/build-release.yml" + - "pyproject.toml" + - "requirements*.txt" + - "scripts/**" workflow_dispatch: inputs: release_mode: @@ -26,11 +32,38 @@ permissions: contents: write concurrency: - group: data-bundle-release + group: data-bundle-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'release' }} cancel-in-progress: false jobs: + validate: + if: github.event_name == 'pull_request' + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out builder + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.13" + cache: pip + - name: Install dependencies + run: | + python -m pip install --disable-pip-version-check --require-hashes --only-binary :all: -r requirements-dev.txt + - name: Check Python sources and offline bundle compatibility + run: | + ruff check scripts + ruff format --check scripts + python -m compileall -q scripts + python -m unittest discover -s scripts -p 'test_*.py' -q + build: + if: github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 90 env: @@ -47,13 +80,15 @@ jobs: cache: pip - name: Install dependencies - run: python -m pip install --disable-pip-version-check --require-hashes --only-binary :all: -r requirements-dev.txt + run: | + python -m pip install --disable-pip-version-check --require-hashes --only-binary :all: -r requirements-dev.txt - name: Check Python sources run: | ruff check scripts ruff format --check scripts python -m compileall -q scripts + python -m unittest discover -s scripts -p 'test_*.py' -q - name: Build Data.zip run: python scripts/build_data.py diff --git a/README.md b/README.md index f867941..c7c836d 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,14 @@ details. ## Build locally -Requires Python 3.10 or later. +Requires Python 3.11 or later (Pillow 13 no longer supports Python 3.10). ```powershell python -m venv .venv .\.venv\Scripts\python -m pip install -r requirements-dev.txt .\.venv\Scripts\ruff check scripts .\.venv\Scripts\ruff format --check scripts +.\.venv\Scripts\python -m unittest discover -s scripts -p 'test_*.py' -q .\.venv\Scripts\python scripts\build_data.py .\.venv\Scripts\python scripts\validate_archive.py build\release\Data.zip ``` @@ -50,6 +51,11 @@ Outputs are written under `build/` and remain untracked. ## Release automation +Pull requests run hash-locked dependency installation, linting, and offline tests +without downloading upstream data or creating releases. The tests check the +patched Pillow pin, PNG/WebP conversion, transparency, slot geometry, and a +deterministic bundle validated independently. + The `Build data bundle` workflow supports: - `validate`: build and retain a workflow artifact without creating a release; diff --git a/pyproject.toml b/pyproject.toml index 4db02d0..bdc1c0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "UP"] diff --git a/requirements.txt b/requirements.txt index 8a5af6c..8973e30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,88 +1,84 @@ -pillow==12.3.0 \ - --hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \ - --hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \ - --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ - --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ - --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ - --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ - --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ - --hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \ - --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ - --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ - --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ - --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ - --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ - --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ - --hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \ - --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ - --hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \ - --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ - --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ - --hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \ - --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ - --hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \ - --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ - --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ - --hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \ - --hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \ - --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ - --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ - --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ - --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ - --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ - --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ - --hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \ - --hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \ - --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ - --hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \ - --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ - --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ - --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ - --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ - --hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \ - --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ - --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ - --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ - --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ - --hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \ - --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ - --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ - --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ - --hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \ - --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ - --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ - --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ - --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ - --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ - --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ - --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ - --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ - --hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \ - --hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \ - --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ - --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ - --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ - --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \ - --hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \ - --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ - --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ - --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ - --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ - --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ - --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ - --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ - --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ - --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ - --hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \ - --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ - --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ - --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ - --hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \ - --hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \ - --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ - --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ - --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ - --hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \ - --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ - --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ - --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 +pillow==13.0.0 \ + --hash=sha256:02464da86f561f410e13b061ec37c3068a9cb557a095494157742d07b3c24a27 \ + --hash=sha256:04a63720f79e22fd54d897c233a68314c50ca0603360f54ce2bd5db8f0f43782 \ + --hash=sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22 \ + --hash=sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15 \ + --hash=sha256:08b826a9bf1265c76de04a42061d61ea6b109418b7826f28ff1ecdfaa26c5502 \ + --hash=sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692 \ + --hash=sha256:10b5019883c98f1dd819ca48ffc6f33a91cee04948fe637c605f4345c7cafd6f \ + --hash=sha256:12b0fa15879fe5a9d9eebe66e4e2d74128a773b1a4448010c9c714f45e04b9d8 \ + --hash=sha256:12fbb851cf323cc19aac3eea1df5ad17b57e91e9fe3cfd06100dba7bb8208c13 \ + --hash=sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3 \ + --hash=sha256:19b66ca0a4d25cc0200b381ce824c7ea090db61427315e73cfb05dbf1b35c369 \ + --hash=sha256:1c774b9c0ba0d0b356f71530677b39028878a2cae5a93e0a13779cfd17120f88 \ + --hash=sha256:20722cd5375533ad754053363a80b8fb30c79d37930a6f7228030e237ce3bd5d \ + --hash=sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb \ + --hash=sha256:21c32061a5512dc13f87cc7303e120e4d77c2f70df89a35af0e3ce246e5ac57f \ + --hash=sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0 \ + --hash=sha256:23edf4ec4b6f4f240e8df6e4985e9ee2ced86474632433e55fb1e2548744bf40 \ + --hash=sha256:2d50500a4a3bc6469adcdb319cf05d688b08ca3bf0f22a05339f3b25b433a599 \ + --hash=sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055 \ + --hash=sha256:34fb3db1149c879343861831728c243c432d4a5afc8f1878ca7612ef8de2a033 \ + --hash=sha256:37290fbef0c73e2d51d594eb78fded7dc7170d1c3ad0a890269e6fd8c8c4e061 \ + --hash=sha256:405be6058e206a86fedce2f715ecc2343f94072d7925251632dffa2c05f70dca \ + --hash=sha256:40a266371ece581e5de13b6635ef78d7487824af80561b6ca27694a88c19bc0f \ + --hash=sha256:42d63be3af4f9b951b199b0a9d23365d37ab14687e6d77b74940432c8515c4ca \ + --hash=sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c \ + --hash=sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4 \ + --hash=sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8 \ + --hash=sha256:4bd024d567eca931b668978d672dba914d4b8e2628e2c3073a2ad178de8a2620 \ + --hash=sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071 \ + --hash=sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc \ + --hash=sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a \ + --hash=sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6 \ + --hash=sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e \ + --hash=sha256:5f56499c1a74d2b862beb0c33b8016f99d095858455df9d21583bbbc1c15b582 \ + --hash=sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f \ + --hash=sha256:67c8d44ad89fa7eb88de5d6ef6199b1e03dd24974f85a67639fc8badb801fe79 \ + --hash=sha256:6a0920cb55df5a4d3f6862964908078c0bd59332bdd1bcd20d14e8c70ef27cc5 \ + --hash=sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683 \ + --hash=sha256:6f38f3f885a09cafc93fd667b1173a92f379e43785d635e7983c893f0da319fb \ + --hash=sha256:73ee25cf8cf8b75d5a5aeed7cb6588f5e5dbfbb0fde51707b630bbb23897645c \ + --hash=sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa \ + --hash=sha256:7c88c10dd432f3b39ccdafc9ed81d0c21b8fd634502574da76d1bfa4fe2c76d0 \ + --hash=sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521 \ + --hash=sha256:7e91e97a929232cdb828889ee6ad0e48d7f40f4ec38818f4e4a8968b2b1af22e \ + --hash=sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c \ + --hash=sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058 \ + --hash=sha256:83b37abd87b590acd8f47ca8a61f70893322b11834d36e07c2298dacad4d14d2 \ + --hash=sha256:8619685cad8efc4c38c66d3b36aad854523297c43d748b3b6f0cc03c90ba3463 \ + --hash=sha256:8bf295fe459343bc6093c8e5e0df60bfa189e0fcb775a22e2c2488fdeccc6c0d \ + --hash=sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81 \ + --hash=sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf \ + --hash=sha256:93ff9c9c3663ce23aaa5d0b6b6abac55359e2a0c06c7e30f3771829af816be63 \ + --hash=sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0 \ + --hash=sha256:9dda0788a9932776a75d8bcc020076edc308ce2508b51f6739a45e3158b3fc42 \ + --hash=sha256:a192360d512305fe92d3053e4f7627bbad1f7708f1f4ad38cccd9336acabe9ad \ + --hash=sha256:a2a14cd73d5e6c927f644b0110ee81f59c4ef506259ec9183904747f2caacc81 \ + --hash=sha256:a625df9a2a5597fa5e5266e01aefc82604bb85d291e3547981e863560d75da6c \ + --hash=sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4 \ + --hash=sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981 \ + --hash=sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43 \ + --hash=sha256:b84be41387fc00db28c13c02e30d42b02d8d8f6a4a741a1aee6012ca7f1f6039 \ + --hash=sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76 \ + --hash=sha256:c3bc8ee0271387e0b1e3ad0ff893645990a656e79bf9c42f4f734a3642b11d51 \ + --hash=sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2 \ + --hash=sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630 \ + --hash=sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac \ + --hash=sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510 \ + --hash=sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6 \ + --hash=sha256:d7710eb676e9bb68965cb045be0d333b4c0d7f9dfbaa7fbf121385b1588683d3 \ + --hash=sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d \ + --hash=sha256:e65dd6e75de67b2362a4fa3e16aa627595f65054dc4ec73d64680c84d592fa61 \ + --hash=sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932 \ + --hash=sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1 \ + --hash=sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d \ + --hash=sha256:e98b4f91f99b24b1da72e10621ba181f7281922384a7403094debb4199de6c00 \ + --hash=sha256:ef5fe6b7db3f501c56000ba8fe7c7e1a35ca1d45824fbeb95a67daa85cff4dbe \ + --hash=sha256:f11a3c1358bb3c68012a7b1f41b223c3db9f7bfde90b481df028cfe1c3363313 \ + --hash=sha256:f391dc92a015c478745192fe4f2e120c2af2aba18d9fe0616341cbf537599806 \ + --hash=sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a \ + --hash=sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0 \ + --hash=sha256:fc481f742ccda20e9ed760b8f978dff50f2a4e9bc253ac167fb0a05b0783ef52 \ + --hash=sha256:ff3fe9eb621fbb9db4c4be33ab946f6bac5f98ab50ca8d3260503653656c961b \ + --hash=sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe diff --git a/scripts/test_build_data.py b/scripts/test_build_data.py new file mode 100644 index 0000000..f22d3ed --- /dev/null +++ b/scripts/test_build_data.py @@ -0,0 +1,109 @@ +"""Offline checks for the pinned decoder and RatEye's generated data contract.""" + +from __future__ import annotations + +import argparse +import contextlib +import io +import json +import re +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import patch + +import PIL +from PIL import Image + +from scripts import build_data, validate_archive + +ROOT = Path(__file__).resolve().parent.parent + + +def image_bytes(mode: str, color: tuple[int, ...], image_format: str = "PNG") -> bytes: + output = io.BytesIO() + Image.new(mode, (64, 64), color).save(output, format=image_format, lossless=True) + return output.getvalue() + + +class DecoderTests(unittest.TestCase): + def test_runtime_and_development_use_the_patched_pillow_pin(self) -> None: + requirement = (ROOT / "requirements.txt").read_text() + match = re.search(r"^pillow==(\d+\.\d+\.\d+)\s", requirement, re.MULTILINE) + self.assertIsNotNone(match) + assert match is not None + version = match.group(1) + self.assertGreaterEqual(tuple(map(int, version.split("."))), (13, 0, 0)) + self.assertEqual(PIL.__version__, version) + self.assertIn("-r requirements.txt", (ROOT / "requirements-dev.txt").read_text()) + + def test_png_and_webp_preserve_pixels_dimensions_and_transparency(self) -> None: + for image_format in ("PNG", "WEBP"): + with self.subTest(image_format=image_format): + png, width, height, transparent = build_data.png_from_image( + image_bytes("RGBA", (10, 20, 30, 128), image_format), "fixture" + ) + self.assertEqual((width, height, transparent), (64, 64, True)) + with Image.open(io.BytesIO(png)) as rendered: + self.assertEqual(rendered.format, "PNG") + self.assertEqual(rendered.mode, "RGBA") + self.assertEqual(rendered.getpixel((0, 0)), (10, 20, 30, 128)) + + def test_opaque_rgb_is_not_reported_as_transparent(self) -> None: + _, width, height, transparent = build_data.png_from_image( + image_bytes("RGB", (10, 20, 30)), "fixture" + ) + self.assertEqual((width, height, transparent), (64, 64, False)) + + def test_invalid_image_is_rejected(self) -> None: + with self.assertRaisesRegex(ValueError, "Could not decode image"): + build_data.png_from_image(b"not an image", "fixture") + + def test_offline_bundle_remains_deterministic_and_independently_valid(self) -> None: + icon_url = "https://assets.tarkov.dev/test-item.webp" + catalog = { + "data": { + "items": [ + {"id": "test-item", "baseImageLink": icon_url, "width": 1, "height": 1}, + {"id": "placeholder", "baseImageLink": build_data.UNKNOWN_IMAGE_URL}, + ] + } + } + sources = { + build_data.CATALOG_URL: json.dumps(catalog).encode(), + icon_url: image_bytes("RGBA", (10, 20, 30, 128), "WEBP"), + build_data.UNKNOWN_IMAGE_URL: image_bytes("RGBA", (0, 0, 0, 0)), + build_data.MAPS_URL: json.dumps([{"normalizedName": "customs", "maps": []}]).encode(), + f"{build_data.TESSDATA_BASE_URL}/eng.traineddata": b"x" * 100_000, + build_data.TARKOV_DEV_LICENSE_URL: b"MIT fixture", + build_data.TESSDATA_LICENSE_URL: b"Apache fixture", + } + with ( + tempfile.TemporaryDirectory() as temporary, + patch.object(build_data, "fetch_bytes", side_effect=lambda url, **_: sources[url]), + patch.object(build_data, "OCR_LANGUAGES", {"eng": "eng"}), + contextlib.redirect_stdout(io.StringIO()), + ): + output = Path(temporary) / "bundle" + arguments = argparse.Namespace( + output=output, catalog_url=build_data.CATALOG_URL, workers=1, minimum_icons=1 + ) + archive = build_data.build(arguments) + first = archive.read_bytes() + validate_archive.validate_archive(str(archive), minimum_icons=1) + with zipfile.ZipFile(archive) as bundle: + manifest = json.loads(bundle.read("manifest.json")) + self.assertEqual(manifest["schemaVersion"], 1) + self.assertEqual(manifest["iconCount"], 1) + self.assertEqual(manifest["skippedItemCount"], 1) + icon = next( + entry for entry in manifest["files"] if entry["path"].startswith("icons/") + ) + self.assertEqual(icon["renderedSlots"], {"width": 1, "height": 1}) + self.assertTrue(icon["slotDimensionsMatch"]) + self.assertEqual(build_data.build(arguments).read_bytes(), first) + + +if __name__ == "__main__": + unittest.main() From 1e7f1120b6b3e696c9baa6af5387661b08bc94e9 Mon Sep 17 00:00:00 2001 From: DysektAI <33468668+DysektAI@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:24:23 -0400 Subject: [PATCH 2/2] test: strengthen offline bundle regression coverage --- .github/workflows/build-release.yml | 2 ++ scripts/test_build_data.py | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index c932610..6260e71 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -6,11 +6,13 @@ on: - master paths: - ".github/workflows/build-release.yml" + - "THIRD_PARTY_NOTICES.md" - "requirements*.txt" - "scripts/**" pull_request: paths: - ".github/workflows/build-release.yml" + - "THIRD_PARTY_NOTICES.md" - "pyproject.toml" - "requirements*.txt" - "scripts/**" diff --git a/scripts/test_build_data.py b/scripts/test_build_data.py index f22d3ed..2e44017 100644 --- a/scripts/test_build_data.py +++ b/scripts/test_build_data.py @@ -21,9 +21,14 @@ ROOT = Path(__file__).resolve().parent.parent -def image_bytes(mode: str, color: tuple[int, ...], image_format: str = "PNG") -> bytes: +def image_bytes( + mode: str, + color: tuple[int, ...], + image_format: str = "PNG", + size: tuple[int, int] = (64, 64), +) -> bytes: output = io.BytesIO() - Image.new(mode, (64, 64), color).save(output, format=image_format, lossless=True) + Image.new(mode, size, color).save(output, format=image_format, lossless=True) return output.getvalue() @@ -39,6 +44,7 @@ def test_runtime_and_development_use_the_patched_pillow_pin(self) -> None: self.assertIn("-r requirements.txt", (ROOT / "requirements-dev.txt").read_text()) def test_png_and_webp_preserve_pixels_dimensions_and_transparency(self) -> None: + """Exercise both decoders without changing the rendered RGBA pixels.""" for image_format in ("PNG", "WEBP"): with self.subTest(image_format=image_format): png, width, height, transparent = build_data.png_from_image( @@ -61,18 +67,19 @@ def test_invalid_image_is_rejected(self) -> None: build_data.png_from_image(b"not an image", "fixture") def test_offline_bundle_remains_deterministic_and_independently_valid(self) -> None: + """Validate asymmetric slot geometry and timestamp-independent ZIP metadata.""" icon_url = "https://assets.tarkov.dev/test-item.webp" catalog = { "data": { "items": [ - {"id": "test-item", "baseImageLink": icon_url, "width": 1, "height": 1}, + {"id": "test-item", "baseImageLink": icon_url, "width": 2, "height": 3}, {"id": "placeholder", "baseImageLink": build_data.UNKNOWN_IMAGE_URL}, ] } } sources = { build_data.CATALOG_URL: json.dumps(catalog).encode(), - icon_url: image_bytes("RGBA", (10, 20, 30, 128), "WEBP"), + icon_url: image_bytes("RGBA", (10, 20, 30, 128), "WEBP", size=(127, 190)), build_data.UNKNOWN_IMAGE_URL: image_bytes("RGBA", (0, 0, 0, 0)), build_data.MAPS_URL: json.dumps([{"normalizedName": "customs", "maps": []}]).encode(), f"{build_data.TESSDATA_BASE_URL}/eng.traineddata": b"x" * 100_000, @@ -100,8 +107,14 @@ def test_offline_bundle_remains_deterministic_and_independently_valid(self) -> N icon = next( entry for entry in manifest["files"] if entry["path"].startswith("icons/") ) - self.assertEqual(icon["renderedSlots"], {"width": 1, "height": 1}) + self.assertEqual((icon["width"], icon["height"]), (127, 190)) + self.assertEqual(icon["renderedSlots"], {"width": 2, "height": 3}) + self.assertEqual(icon["declaredSlots"], {"width": 2, "height": 3}) self.assertTrue(icon["slotDimensionsMatch"]) + with Image.open(io.BytesIO(bundle.read(icon["path"]))) as rendered: + self.assertEqual(rendered.size, (127, 190)) + for entry in bundle.infolist(): + self.assertEqual(entry.date_time, (1980, 1, 1, 0, 0, 0)) self.assertEqual(build_data.build(arguments).read_bytes(), first)