You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every fixture member is 4–14 bytes, below the divergence threshold. The regression tests that exist to protect these hashes cannot detect the one thing that breaks them.
Expected behavior
The same input content produces the same archive bytes on every supported interpreter and platform.
The invariant is reference zlib raw DEFLATE at level 6, memLevel 8, default strategy.
Studio's frontend independently produces those same bytes via pako ({level: 6, windowBits: -15, memLevel: 8, strategy: 0}).
Every file already in Studio was hashed from them.
User-facing consequences
Chefs on an affected interpreter produce archives whose MD5s differ from the ones Studio already holds for identical content.
Deduplication misses, so identical content is stored twice.
Every affected node's checksum changes.
Learners re-download content that did not change.
That cost lands hardest on the low-bandwidth, offline-first deployments Kolibri targets.
Nothing errors at runtime, because neither side compares hashes during upload.
The mismatch stays silent until someone compares hashes by hand.
The same five pinned MD5s are asserted in tests/test_zip.py and in Studio's zipFile.spec.js.
Neither test suite can detect a compressor change on either side of the contract.
ricecooker then refuses to process HTML5, ePub and KPUB content on any zlib-ng interpreter, including Fedora's system Python.
The guard fires inside create_predictable_zip, so chefs fail mid-run after everything has downloaded.
Steps to reproduce
On an interpreter linked against zlib-ng — Fedora 40+ system Python, or CPython 3.14 on Windows — confirm the build: python -c "import zlib; print(zlib.ZLIB_RUNTIME_VERSION)" reports 1.3.1.zlib-ng.
Create a directory containing a single file larger than ~256 bytes, e.g. 30 repetitions of <p>Kolibri learning content</p>.
Run create_predictable_zip on that directory and take the MD5 of the result.
Repeat steps 2–3 on an interpreter linked against reference zlib — any uv-managed CPython, which statically bundles zlib 1.3.1.
Observe that the two MD5s differ.
Replace the file with an 11-byte one and observe that they now agree.
Note that uv run --group test pytest tests/test_zip.py passes on both interpreters throughout.
Without access to a zlib-ng interpreter, pip install zlib-ng and assign zipfile.zlib = zlib_ng.zlib_ng before building reproduces the same divergence.
That is why CI and local development have not caught this.
The affected surface is a packaging trend, not one platform.
Fedora has already switched its system libz.
CPython's Windows 3.14 build is the same change arriving from a different direction.
Possible approaches
Not decided. These two are genuine alternatives.
Nothing off the shelf produces reference-zlib bytes, measured on a 28-input corpus of repo fixtures:
gzip -6: 18/28 exact.
Info-ZIP zip -6: 18/28 exact.
zstd --format=gzip -6: 0/28.
deflate wheel (libdeflate): 1/28.
imagecodecs.zlib_encode: 1/28, because it links system libz.
So option 1 in either form means building madler zlib ourselves. Both forms mean never taking a deflate-output-changing update to it.
Carry our own reference zlib, so output stops depending on the interpreter's build. Distributed as:
a. A shared library as package data, driven by ctypes.
Verified byte-exact against its linked library on 37/37 inputs, at 32.6 MB/s.
No C extension, so wheels vary by platform only.
Needs no compiler and no user action.
b. A C extension wheel.
Fastest and most idiomatic.
Adds a cibuildwheel matrix over 3.10–3.14 × 3 OS × 2 arch.
sdist installs then need a compiler.
Stop deriving content identity from compressed bytes.
Hash sorted (path, sha256(member)) pairs, so the compressor stops mattering.
Ends the problem class.
Studio must accept a second identity alongside the file MD5.
Acceptance Criteria
create_predictable_zip produces identical bytes for identical input on reference-zlib and zlib-ng interpreters.
Those bytes match the MD5s currently pinned in tests/test_zip.py.
Those bytes match what Studio's frontend produces via pako.
A non-reference zlib build is detected by comparing compressed output against pinned vectors, not by reading version strings.
Forks that do not self-identify as zlib-ng are caught.
The pinned fixtures include at least one member over 1 KB.
The pinned fixtures fail when the compressor is swapped for zlib-ng.
CI covers an interpreter linked against zlib-ng.
windows-latest, 3.14 is restored to the CI matrix, or this issue records why it stays excluded.
HTML5, ePub and KPUB content processes successfully on Fedora system Python.
AI usage
Investigated with Claude Code. Every measurement here was produced by running it locally rather than taken from the model's knowledge.
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Target branch: main
Observed behavior
create_predictable_zipoutput bytes depend on which zlib build the running interpreter links.Errors and logs
Fedora 44's system interpreter, which ships zlib-ng as the system libz:
The same 956-byte
index.htmlzipped under each implementation:Rebuilding every pinned case in
tests/test_zip.pywith zlib-ng as the compressor:Every fixture member is 4–14 bytes, below the divergence threshold. The regression tests that exist to protect these hashes cannot detect the one thing that breaks them.
Expected behavior
{level: 6, windowBits: -15, memLevel: 8, strategy: 0}).User-facing consequences
tests/test_zip.pyand in Studio'szipFile.spec.js.create_predictable_zip, so chefs fail mid-run after everything has downloaded.Steps to reproduce
python -c "import zlib; print(zlib.ZLIB_RUNTIME_VERSION)"reports1.3.1.zlib-ng.<p>Kolibri learning content</p>.create_predictable_zipon that directory and take the MD5 of the result.uv run --group test pytest tests/test_zip.pypasses on both interpreters throughout.Without access to a zlib-ng interpreter,
pip install zlib-ngand assignzipfile.zlib = zlib_ng.zlib_ngbefore building reproduces the same divergence.Context
main, plus PR Drop Python 3.9, add Python 3.14 #720 (branchissue-719-621d1e).zlib-ng-compat2.3.3.Possible approaches
Not decided. These two are genuine alternatives.
Nothing off the shelf produces reference-zlib bytes, measured on a 28-input corpus of repo fixtures:
gzip -6: 18/28 exact.zip -6: 18/28 exact.zstd --format=gzip -6: 0/28.deflatewheel (libdeflate): 1/28.imagecodecs.zlib_encode: 1/28, because it links system libz.So option 1 in either form means building madler zlib ourselves. Both forms mean never taking a deflate-output-changing update to it.
Carry our own reference zlib, so output stops depending on the interpreter's build. Distributed as:
a. A shared library as package data, driven by ctypes.
b. A C extension wheel.
Stop deriving content identity from compressed bytes.
(path, sha256(member))pairs, so the compressor stops mattering.Acceptance Criteria
create_predictable_zipproduces identical bytes for identical input on reference-zlib and zlib-ng interpreters.tests/test_zip.py.windows-latest, 3.14is restored to the CI matrix, or this issue records why it stays excluded.AI usage
Investigated with Claude Code. Every measurement here was produced by running it locally rather than taken from the model's knowledge.