Skip to main content

lzstring-codec

lz-string for Python, held byte-for-byte to the JavaScript original.

Installed as lzstring-codec, imported as lz_string. The obvious name is not available: PyPI refuses it as too similar to lzstring, which is one of the two packages this exists to replace.

It exists because the package that is on PyPI gets the format wrong in ways that quietly corrupt real save files. It was written for saveeditor.online, which reads a few thousand lz-string saves a week and cannot afford to hand any of them back altered.

import lz_string as lz

lz.compress_to_base64('{"gold":9000}')          # 'N4Ig5g9gNgJiBcBOADKgvkA='
lz.decompress_from_base64(_)                    # '{"gold":9000}'

Why

lzstring 1.0.4 on PyPI is a line-by-line transliteration of the JavaScript, and it inherits neither its speed nor its correctness. Measured against 1202 vectors produced by lz-string 1.5.0 on npm — the library the games themselves use:

  • 294 vectors compress to different bytes, because it walks Python code points where the format is defined over UTF-16 code units. In practice: an emoji in a player's name is written truncated, and the game reads back U+F600, a private-use box. Every save with an astral character is corrupted on write.
  • decompressFromUTF16 raises TypeError on every input. It subtracts an integer from a string. The function has never worked.
  • Decompression rebuilds a 65-entry alphabet table per character and reads the bit stream one bit at a time: 22 seconds for a 1.8 MB save, on the request path.

This package is byte-identical to the reference on all 1202 vectors, in all four transports, in both directions, on both backends — and 15–45× faster.

How far that was pushed, because "it round-trips" is not the same as "it is right":

check scale result
golden corpus vs. node 1202 vectors x 4 transports x 2 directions x 2 backends identical
real production saves 38 files up to 1.8 MB, decompress and recompress identical
differential fuzz vs. node 5000 generated inputs x 4 transports x 2 backends identical
malformed-input fuzz 16000 probes x 2 backends agree with each other and with the reference; nothing raised

The last row is the one that found bugs — two of them in this package, both invisible to every check above it, and both now pinned by tests. SPEC.md §4 says what they were.

And it is not the only one: py-lzstring 0.1.1, a separate port of the same reference, differs from it on 295 of the same 1202 vectors, first on exactly the same astral character. Two independent Python ports, the same mistake in both.

See SPEC.md for the format itself and for the full divergence table.

Installing

The package is a compiled extension; pip install builds it, and a Rust toolchain is required to do so.

uv add git+ssh://git@github.com/Derfirm/lz-string.git    # or, in a checkout:
uv sync                                                  # builds the extension in place
pip install .                                            # pip works too; maturin does the build

An import without the extension raises rather than falling back to something slower and subtly different.

Build it where it will run. The binary is tied to the C library it was linked against: one built on Debian bookworm (glibc 2.36) will not load on bullseye (2.31), which is what python:3.12.11-slim-bullseye — the image this package is destined for — is built on. It fails at import with GLIBC_2.34 not found, not at build time. Either build inside the target image, or produce a manylinux wheel.

On two real saves, best of three in one run:

decompress compress
this package, 259 KB → 975 K chars 0.004 s 0.044 s
lzstring 1.0.4, the same 1.199 s 0.312 s
this package, 1.8 MB → 13.4 M chars 0.054 s 1.547 s
lzstring 1.0.4, the same 8.581 s 4.766 s

Both halves are ours, and the only Rust dependency is pyo3. The obvious alternative, the lz-str crate, was the starting point and did not survive contact with damaged input: it answers the same thing for two failures the reference keeps apart, skips characters the reference reads as zero bits, and refuses a payload whose trailing padding character was trimmed. SPEC.md §7 has the measurements.

A pure-Python implementation of the whole format lives in src/lz_string/_reference.py. It is the test suite's second opinion, not a fallback: nothing imports it at runtime.

Migrating from lzstring

The camelCase API is provided as-is, so the change is one import line:

-from lzstring import LZString
+from lz_string import LZString

Two behaviour changes to know about, both improvements, both in SPEC.md: saves containing astral characters stop being corrupted, and decompressFromUTF16 starts working.

Tests

uv sync --extra dev                      # .python-version pins 3.12, as in production
uv run pytest                            # every test runs against both implementations
uv sync --reinstall-package lzstring-codec   # after touching the Rust: rebuild it
./tools/check_linux.sh                   # built and run inside the target image (needs docker)
./tools/check_linux.sh linux/amd64       # and on production's architecture, cross-compiled

The Linux run is not ceremony. It is done in python:3.12.11-slim-bullseye — the exact image this is destined for — and it has already caught two things a laptop cannot: a test that measured the machine rather than the code, and an extension that built cleanly and then refused to load because it wanted a newer glibc than the image has.

251 tests: the golden corpus per category and transport, the documented behaviour on damaged input, seeded round-trips over random code units and surrogate halves, parity between the shipped extension and the Python reference — including a fuzz of malformed payloads, which is what pins the three corrections the crate underneath needed — and two tests that the extension really does let go of the interpreter while it works.

To regenerate the corpus (deterministic — a diff means the reference moved):

npm --prefix tools install
node tools/gen_vectors.mjs

tools/bench.py reproduces the numbers above.

Releasing

By tag, not by merge. Most merges here are documentation and CI, and PyPI refuses a second upload of the same version — publishing on merge would mean either a version bump per commit or a release job that fails as a matter of course.

# bump `version` in pyproject.toml, note it in CHANGELOG.md, merge, then:
git tag v0.1.0 && git push origin v0.1.0

The workflow builds wheels for linux and macOS on both architectures plus an sdist, checks that the tag and the version in pyproject.toml agree, and only then uploads. workflow_dispatch runs everything except the upload, which is how the pipeline is tested without releasing anything.

The upload needs a credential, and there are two ways to give it one. Either works; the first stores nothing.

Trusted publishing. On PyPI, add a pending publisher for the project name with owner Derfirm, repository lz-string, workflow release.yml, environment pypi. Nothing to configure here — with no PYPI_API_TOKEN secret set, the publish step already exchanges a short-lived OIDC token, which is what it tries today and what fails with "Trusted publishing exchange failure" until the publisher exists.

A token. Create an environment named pypi in the repository settings, put an API token in it as PYPI_API_TOKEN, and re-run the publish job of the tagged release. Protection rules on that environment are where you add an approval step if you want one.

Provenance and licence

MIT (LICENSE). Compression comes from the lz-str crate and the bindings from pyo3, both MIT/Apache-2.0; the decoder is a port of pieroxy/lz-string, MIT, which is also the reference every claim here is measured against. See THIRD_PARTY.md.

No user data lives in this repository: every test input is generated, and the corpus is the reference implementation's own output.

Metadata

Release files for lzstring-codec 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lzstring-codec 0.1.0
File Size Uploaded Provenance
lzstring_codec-0.1.0.tar.gz 18.5 kB Without provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for lzstring-codec 0.1.0
File
lzstring_codec-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.12 abi3 Linux glibc 2.17+ x86-64 Details
lzstring_codec-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.12 abi3 Linux glibc 2.17+ ARM64 Details
lzstring_codec-0.1.0-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
lzstring_codec-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl CPython 3.12 abi3 macOS 10.12+ x86-64 Details

Total release size: 907.7 kB

Release files / lzstring_codec-0.1.0.tar.gz

Download URL lzstring_codec-0.1.0.tar.gz
Size 18.5 kB
Tags Source
SHA-256 checksum
How to use checksums
13039167779a70f3ce67af5b13e9cae86640f0312a7e99c76f60fe774fb4fb3a
BLAKE2b-256 checksum
How to use checksums
e2809e1e7d989f4c74c109f0cc9b9309673307d5c34b3fe28e48142d26e95216
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / lzstring_codec-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL lzstring_codec-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 230.3 kB
Tags CPython 3.12 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
39ee9c82f7c807eb20b4b5922c79f06f634753295bc62cbb569aedf69ee1e086
BLAKE2b-256 checksum
How to use checksums
ac22e996362170f6e0bc3f49dca2a1b53f8b543d7c9b83fa5ea21715658b163d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / lzstring_codec-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL lzstring_codec-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 229.9 kB
Tags CPython 3.12 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
b0be765af22576fb1f7fbc5deca3ff653c965a8b9e88dc8ba7246992064eef90
BLAKE2b-256 checksum
How to use checksums
2de6fe1b4e08f71980116a9c5b4941a50c561abd5bfa0210ca48394c414a1fb6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / lzstring_codec-0.1.0-cp312-abi3-macosx_11_0_arm64.whl

Download URL lzstring_codec-0.1.0-cp312-abi3-macosx_11_0_arm64.whl
Size 210.3 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
afeb89a28303d4c989c90d43068650a715a38b176a56cadf022848894d64e125
BLAKE2b-256 checksum
How to use checksums
b1bc5fb084495d424ead97fda06cca60f91a7c3ff7138786a8f23beecb25a7c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / lzstring_codec-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl

Download URL lzstring_codec-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl
Size 218.8 kB
Tags CPython 3.12 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
65f415cec4c3471c2cc2d634050177301a4b208f0e794a9c1eff83fc768b67d3
BLAKE2b-256 checksum
How to use checksums
4b68bee4a6e89c347e8c32fb96c9c6f1467c7611b80749a1b1dc060c7d03402e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for lzstring-codec 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

5 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page