lzstring-codec 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lzstring_codec-0.1.0/LICENSE +21 -0
- lzstring_codec-0.1.0/PKG-INFO +202 -0
- lzstring_codec-0.1.0/README.md +174 -0
- lzstring_codec-0.1.0/THIRD_PARTY.md +23 -0
- lzstring_codec-0.1.0/pyproject.toml +55 -0
- lzstring_codec-0.1.0/rust/Cargo.lock +132 -0
- lzstring_codec-0.1.0/rust/Cargo.toml +16 -0
- lzstring_codec-0.1.0/rust/src/decode.rs +159 -0
- lzstring_codec-0.1.0/rust/src/encode.rs +170 -0
- lzstring_codec-0.1.0/rust/src/lib.rs +192 -0
- lzstring_codec-0.1.0/src/lz_string/__init__.py +158 -0
- lzstring_codec-0.1.0/src/lz_string/_reference.py +340 -0
- lzstring_codec-0.1.0/src/lz_string/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Grinevich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lzstring-codec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Classifier: Programming Language :: Rust
|
|
10
|
+
Classifier: Topic :: System :: Archiving :: Compression
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Dist: pytest>=8 ; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.6 ; extra == 'dev'
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
License-File: THIRD_PARTY.md
|
|
17
|
+
Summary: lz-string for Python, held byte-for-byte to the JavaScript original
|
|
18
|
+
Keywords: lz-string,lzstring,lzstring-codec,compression,lzw,savegame,utf-16
|
|
19
|
+
Author-email: Andrew Grinevich <beulea@gmail.com>
|
|
20
|
+
License-Expression: MIT
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
23
|
+
Project-URL: Homepage, https://github.com/Derfirm/lz-string
|
|
24
|
+
Project-URL: Issues, https://github.com/Derfirm/lz-string/issues
|
|
25
|
+
Project-URL: Source, https://github.com/Derfirm/lz-string
|
|
26
|
+
Project-URL: Specification, https://github.com/Derfirm/lz-string/blob/master/SPEC.md
|
|
27
|
+
|
|
28
|
+
# lzstring-codec
|
|
29
|
+
|
|
30
|
+
lz-string for Python, held byte-for-byte to the JavaScript original.
|
|
31
|
+
|
|
32
|
+
Installed as `lzstring-codec`, imported as `lz_string`. The obvious name is not available:
|
|
33
|
+
PyPI refuses it as too similar to `lzstring`, which is one of the two packages this exists
|
|
34
|
+
to replace.
|
|
35
|
+
|
|
36
|
+
It exists because the package that *is* on PyPI gets the format wrong in ways that quietly
|
|
37
|
+
corrupt real save files. It was written for [saveeditor.online](https://saveeditor.online),
|
|
38
|
+
which reads a few thousand lz-string saves a week and cannot afford to hand any of them back
|
|
39
|
+
altered.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import lz_string as lz
|
|
43
|
+
|
|
44
|
+
lz.compress_to_base64('{"gold":9000}') # 'N4Ig5g9gNgJiBcBOADKgvkA='
|
|
45
|
+
lz.decompress_from_base64(_) # '{"gold":9000}'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Why
|
|
49
|
+
|
|
50
|
+
`lzstring` 1.0.4 on PyPI is a line-by-line transliteration of the JavaScript, and it
|
|
51
|
+
inherits neither its speed nor its correctness. Measured against 1202 vectors produced by
|
|
52
|
+
`lz-string` 1.5.0 on npm — the library the games themselves use:
|
|
53
|
+
|
|
54
|
+
- **294 vectors compress to different bytes**, because it walks Python code points where
|
|
55
|
+
the format is defined over UTF-16 code units. In practice: an emoji in a player's name is
|
|
56
|
+
written truncated, and the game reads back `U+F600`, a private-use box. Every save with an
|
|
57
|
+
astral character is corrupted on write.
|
|
58
|
+
- **`decompressFromUTF16` raises `TypeError` on every input.** It subtracts an integer from
|
|
59
|
+
a string. The function has never worked.
|
|
60
|
+
- Decompression rebuilds a 65-entry alphabet table *per character* and reads the bit stream
|
|
61
|
+
one bit at a time: **22 seconds** for a 1.8 MB save, on the request path.
|
|
62
|
+
|
|
63
|
+
This package is byte-identical to the reference on all 1202 vectors, in all four
|
|
64
|
+
transports, in both directions, on both backends — and 15–45× faster.
|
|
65
|
+
|
|
66
|
+
How far that was pushed, because "it round-trips" is not the same as "it is right":
|
|
67
|
+
|
|
68
|
+
| check | scale | result |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| golden corpus vs. node | 1202 vectors x 4 transports x 2 directions x 2 backends | identical |
|
|
71
|
+
| real production saves | 38 files up to 1.8 MB, decompress **and** recompress | identical |
|
|
72
|
+
| differential fuzz vs. node | 5000 generated inputs x 4 transports x 2 backends | identical |
|
|
73
|
+
| malformed-input fuzz | 16000 probes x 2 backends | agree with each other and with the reference; nothing raised |
|
|
74
|
+
|
|
75
|
+
The last row is the one that found bugs — two of them in this package, both invisible to
|
|
76
|
+
every check above it, and both now pinned by tests. SPEC.md §4 says what they were.
|
|
77
|
+
|
|
78
|
+
And it is not the only one: `py-lzstring` 0.1.1, a separate port of the same reference,
|
|
79
|
+
differs from it on **295 of the same 1202 vectors**, first on exactly the same astral
|
|
80
|
+
character. Two independent Python ports, the same mistake in both.
|
|
81
|
+
|
|
82
|
+
See [SPEC.md](SPEC.md) for the format itself and for the full divergence table.
|
|
83
|
+
|
|
84
|
+
## Installing
|
|
85
|
+
|
|
86
|
+
The package is a compiled extension; `pip install` builds it, and a Rust toolchain is
|
|
87
|
+
required to do so.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv add git+ssh://git@github.com/Derfirm/lz-string.git # or, in a checkout:
|
|
91
|
+
uv sync # builds the extension in place
|
|
92
|
+
pip install . # pip works too; maturin does the build
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
An import without the extension raises rather than falling back to something slower and
|
|
96
|
+
subtly different.
|
|
97
|
+
|
|
98
|
+
**Build it where it will run.** The binary is tied to the C library it was linked against:
|
|
99
|
+
one built on Debian bookworm (glibc 2.36) will not load on bullseye (2.31), which is what
|
|
100
|
+
`python:3.12.11-slim-bullseye` — the image this package is destined for — is built on. It
|
|
101
|
+
fails at import with `GLIBC_2.34 not found`, not at build time. Either build inside the
|
|
102
|
+
target image, or produce a manylinux wheel.
|
|
103
|
+
|
|
104
|
+
On two real saves, best of three in one run:
|
|
105
|
+
|
|
106
|
+
| | decompress | compress |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| **this package**, 259 KB → 975 K chars | **0.004 s** | **0.044 s** |
|
|
109
|
+
| `lzstring` 1.0.4, the same | 1.199 s | 0.312 s |
|
|
110
|
+
| **this package**, 1.8 MB → 13.4 M chars | **0.054 s** | **1.547 s** |
|
|
111
|
+
| `lzstring` 1.0.4, the same | 8.581 s | 4.766 s |
|
|
112
|
+
|
|
113
|
+
Both halves are ours, and the only Rust dependency is pyo3. The obvious alternative,
|
|
114
|
+
the [`lz-str`](https://crates.io/crates/lz-str) crate, was the starting point and did not
|
|
115
|
+
survive contact with damaged input: it answers the same thing for two failures the reference
|
|
116
|
+
keeps apart, skips characters the reference reads as zero bits, and refuses a payload whose
|
|
117
|
+
trailing padding character was trimmed. SPEC.md §7 has the measurements.
|
|
118
|
+
|
|
119
|
+
A pure-Python implementation of the whole format lives in `src/lz_string/_reference.py`.
|
|
120
|
+
It is the test suite's second opinion, not a fallback: nothing imports it at runtime.
|
|
121
|
+
|
|
122
|
+
## Migrating from `lzstring`
|
|
123
|
+
|
|
124
|
+
The camelCase API is provided as-is, so the change is one import line:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
-from lzstring import LZString
|
|
128
|
+
+from lz_string import LZString
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Two behaviour changes to know about, both improvements, both in SPEC.md: saves containing
|
|
132
|
+
astral characters stop being corrupted, and `decompressFromUTF16` starts working.
|
|
133
|
+
|
|
134
|
+
## Tests
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
uv sync --extra dev # .python-version pins 3.12, as in production
|
|
138
|
+
uv run pytest # every test runs against both implementations
|
|
139
|
+
uv sync --reinstall-package lzstring-codec # after touching the Rust: rebuild it
|
|
140
|
+
./tools/check_linux.sh # built and run inside the target image (needs docker)
|
|
141
|
+
./tools/check_linux.sh linux/amd64 # and on production's architecture, cross-compiled
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The Linux run is not ceremony. It is done in `python:3.12.11-slim-bullseye` — the exact image
|
|
145
|
+
this is destined for — and it has already caught two things a laptop cannot: a test that
|
|
146
|
+
measured the machine rather than the code, and an extension that built cleanly and then
|
|
147
|
+
refused to load because it wanted a newer glibc than the image has.
|
|
148
|
+
|
|
149
|
+
251 tests: the golden corpus per category and transport, the documented behaviour on damaged
|
|
150
|
+
input, seeded round-trips over random code units and surrogate halves, parity between the
|
|
151
|
+
shipped extension and the Python reference — including a fuzz of malformed payloads, which
|
|
152
|
+
is what pins the three corrections the crate underneath needed — and two tests that the
|
|
153
|
+
extension really does let go of the interpreter while it works.
|
|
154
|
+
|
|
155
|
+
To regenerate the corpus (deterministic — a diff means the reference moved):
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm --prefix tools install
|
|
159
|
+
node tools/gen_vectors.mjs
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`tools/bench.py` reproduces the numbers above.
|
|
163
|
+
|
|
164
|
+
## Releasing
|
|
165
|
+
|
|
166
|
+
By tag, not by merge. Most merges here are documentation and CI, and PyPI refuses a second
|
|
167
|
+
upload of the same version — publishing on merge would mean either a version bump per commit
|
|
168
|
+
or a release job that fails as a matter of course.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# bump `version` in pyproject.toml, note it in CHANGELOG.md, merge, then:
|
|
172
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The workflow builds wheels for linux and macOS on both architectures plus an sdist, checks
|
|
176
|
+
that the tag and the version in pyproject.toml agree, and only then uploads.
|
|
177
|
+
`workflow_dispatch` runs everything except the upload, which is how the pipeline is tested
|
|
178
|
+
without releasing anything.
|
|
179
|
+
|
|
180
|
+
The upload needs a credential, and there are two ways to give it one. Either works; the
|
|
181
|
+
first stores nothing.
|
|
182
|
+
|
|
183
|
+
**Trusted publishing.** On PyPI, add a pending publisher for the project name with owner
|
|
184
|
+
`Derfirm`, repository `lz-string`, workflow `release.yml`, environment `pypi`. Nothing to
|
|
185
|
+
configure here — with no `PYPI_API_TOKEN` secret set, the publish step already exchanges a
|
|
186
|
+
short-lived OIDC token, which is what it tries today and what fails with "Trusted publishing
|
|
187
|
+
exchange failure" until the publisher exists.
|
|
188
|
+
|
|
189
|
+
**A token.** Create an environment named `pypi` in the repository settings, put an API token
|
|
190
|
+
in it as `PYPI_API_TOKEN`, and re-run the publish job of the tagged release. Protection
|
|
191
|
+
rules on that environment are where you add an approval step if you want one.
|
|
192
|
+
|
|
193
|
+
## Provenance and licence
|
|
194
|
+
|
|
195
|
+
MIT (`LICENSE`). Compression comes from the [`lz-str`](https://crates.io/crates/lz-str)
|
|
196
|
+
crate and the bindings from [pyo3](https://pyo3.rs), both MIT/Apache-2.0; the decoder is a
|
|
197
|
+
port of [pieroxy/lz-string](https://github.com/pieroxy/lz-string), MIT, which is also the
|
|
198
|
+
reference every claim here is measured against. See `THIRD_PARTY.md`.
|
|
199
|
+
|
|
200
|
+
No user data lives in this repository: every test input is generated, and the corpus is the
|
|
201
|
+
reference implementation's own output.
|
|
202
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# lzstring-codec
|
|
2
|
+
|
|
3
|
+
lz-string for Python, held byte-for-byte to the JavaScript original.
|
|
4
|
+
|
|
5
|
+
Installed as `lzstring-codec`, imported as `lz_string`. The obvious name is not available:
|
|
6
|
+
PyPI refuses it as too similar to `lzstring`, which is one of the two packages this exists
|
|
7
|
+
to replace.
|
|
8
|
+
|
|
9
|
+
It exists because the package that *is* on PyPI gets the format wrong in ways that quietly
|
|
10
|
+
corrupt real save files. It was written for [saveeditor.online](https://saveeditor.online),
|
|
11
|
+
which reads a few thousand lz-string saves a week and cannot afford to hand any of them back
|
|
12
|
+
altered.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import lz_string as lz
|
|
16
|
+
|
|
17
|
+
lz.compress_to_base64('{"gold":9000}') # 'N4Ig5g9gNgJiBcBOADKgvkA='
|
|
18
|
+
lz.decompress_from_base64(_) # '{"gold":9000}'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
`lzstring` 1.0.4 on PyPI is a line-by-line transliteration of the JavaScript, and it
|
|
24
|
+
inherits neither its speed nor its correctness. Measured against 1202 vectors produced by
|
|
25
|
+
`lz-string` 1.5.0 on npm — the library the games themselves use:
|
|
26
|
+
|
|
27
|
+
- **294 vectors compress to different bytes**, because it walks Python code points where
|
|
28
|
+
the format is defined over UTF-16 code units. In practice: an emoji in a player's name is
|
|
29
|
+
written truncated, and the game reads back `U+F600`, a private-use box. Every save with an
|
|
30
|
+
astral character is corrupted on write.
|
|
31
|
+
- **`decompressFromUTF16` raises `TypeError` on every input.** It subtracts an integer from
|
|
32
|
+
a string. The function has never worked.
|
|
33
|
+
- Decompression rebuilds a 65-entry alphabet table *per character* and reads the bit stream
|
|
34
|
+
one bit at a time: **22 seconds** for a 1.8 MB save, on the request path.
|
|
35
|
+
|
|
36
|
+
This package is byte-identical to the reference on all 1202 vectors, in all four
|
|
37
|
+
transports, in both directions, on both backends — and 15–45× faster.
|
|
38
|
+
|
|
39
|
+
How far that was pushed, because "it round-trips" is not the same as "it is right":
|
|
40
|
+
|
|
41
|
+
| check | scale | result |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| golden corpus vs. node | 1202 vectors x 4 transports x 2 directions x 2 backends | identical |
|
|
44
|
+
| real production saves | 38 files up to 1.8 MB, decompress **and** recompress | identical |
|
|
45
|
+
| differential fuzz vs. node | 5000 generated inputs x 4 transports x 2 backends | identical |
|
|
46
|
+
| malformed-input fuzz | 16000 probes x 2 backends | agree with each other and with the reference; nothing raised |
|
|
47
|
+
|
|
48
|
+
The last row is the one that found bugs — two of them in this package, both invisible to
|
|
49
|
+
every check above it, and both now pinned by tests. SPEC.md §4 says what they were.
|
|
50
|
+
|
|
51
|
+
And it is not the only one: `py-lzstring` 0.1.1, a separate port of the same reference,
|
|
52
|
+
differs from it on **295 of the same 1202 vectors**, first on exactly the same astral
|
|
53
|
+
character. Two independent Python ports, the same mistake in both.
|
|
54
|
+
|
|
55
|
+
See [SPEC.md](SPEC.md) for the format itself and for the full divergence table.
|
|
56
|
+
|
|
57
|
+
## Installing
|
|
58
|
+
|
|
59
|
+
The package is a compiled extension; `pip install` builds it, and a Rust toolchain is
|
|
60
|
+
required to do so.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv add git+ssh://git@github.com/Derfirm/lz-string.git # or, in a checkout:
|
|
64
|
+
uv sync # builds the extension in place
|
|
65
|
+
pip install . # pip works too; maturin does the build
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
An import without the extension raises rather than falling back to something slower and
|
|
69
|
+
subtly different.
|
|
70
|
+
|
|
71
|
+
**Build it where it will run.** The binary is tied to the C library it was linked against:
|
|
72
|
+
one built on Debian bookworm (glibc 2.36) will not load on bullseye (2.31), which is what
|
|
73
|
+
`python:3.12.11-slim-bullseye` — the image this package is destined for — is built on. It
|
|
74
|
+
fails at import with `GLIBC_2.34 not found`, not at build time. Either build inside the
|
|
75
|
+
target image, or produce a manylinux wheel.
|
|
76
|
+
|
|
77
|
+
On two real saves, best of three in one run:
|
|
78
|
+
|
|
79
|
+
| | decompress | compress |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| **this package**, 259 KB → 975 K chars | **0.004 s** | **0.044 s** |
|
|
82
|
+
| `lzstring` 1.0.4, the same | 1.199 s | 0.312 s |
|
|
83
|
+
| **this package**, 1.8 MB → 13.4 M chars | **0.054 s** | **1.547 s** |
|
|
84
|
+
| `lzstring` 1.0.4, the same | 8.581 s | 4.766 s |
|
|
85
|
+
|
|
86
|
+
Both halves are ours, and the only Rust dependency is pyo3. The obvious alternative,
|
|
87
|
+
the [`lz-str`](https://crates.io/crates/lz-str) crate, was the starting point and did not
|
|
88
|
+
survive contact with damaged input: it answers the same thing for two failures the reference
|
|
89
|
+
keeps apart, skips characters the reference reads as zero bits, and refuses a payload whose
|
|
90
|
+
trailing padding character was trimmed. SPEC.md §7 has the measurements.
|
|
91
|
+
|
|
92
|
+
A pure-Python implementation of the whole format lives in `src/lz_string/_reference.py`.
|
|
93
|
+
It is the test suite's second opinion, not a fallback: nothing imports it at runtime.
|
|
94
|
+
|
|
95
|
+
## Migrating from `lzstring`
|
|
96
|
+
|
|
97
|
+
The camelCase API is provided as-is, so the change is one import line:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
-from lzstring import LZString
|
|
101
|
+
+from lz_string import LZString
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Two behaviour changes to know about, both improvements, both in SPEC.md: saves containing
|
|
105
|
+
astral characters stop being corrupted, and `decompressFromUTF16` starts working.
|
|
106
|
+
|
|
107
|
+
## Tests
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv sync --extra dev # .python-version pins 3.12, as in production
|
|
111
|
+
uv run pytest # every test runs against both implementations
|
|
112
|
+
uv sync --reinstall-package lzstring-codec # after touching the Rust: rebuild it
|
|
113
|
+
./tools/check_linux.sh # built and run inside the target image (needs docker)
|
|
114
|
+
./tools/check_linux.sh linux/amd64 # and on production's architecture, cross-compiled
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The Linux run is not ceremony. It is done in `python:3.12.11-slim-bullseye` — the exact image
|
|
118
|
+
this is destined for — and it has already caught two things a laptop cannot: a test that
|
|
119
|
+
measured the machine rather than the code, and an extension that built cleanly and then
|
|
120
|
+
refused to load because it wanted a newer glibc than the image has.
|
|
121
|
+
|
|
122
|
+
251 tests: the golden corpus per category and transport, the documented behaviour on damaged
|
|
123
|
+
input, seeded round-trips over random code units and surrogate halves, parity between the
|
|
124
|
+
shipped extension and the Python reference — including a fuzz of malformed payloads, which
|
|
125
|
+
is what pins the three corrections the crate underneath needed — and two tests that the
|
|
126
|
+
extension really does let go of the interpreter while it works.
|
|
127
|
+
|
|
128
|
+
To regenerate the corpus (deterministic — a diff means the reference moved):
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm --prefix tools install
|
|
132
|
+
node tools/gen_vectors.mjs
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`tools/bench.py` reproduces the numbers above.
|
|
136
|
+
|
|
137
|
+
## Releasing
|
|
138
|
+
|
|
139
|
+
By tag, not by merge. Most merges here are documentation and CI, and PyPI refuses a second
|
|
140
|
+
upload of the same version — publishing on merge would mean either a version bump per commit
|
|
141
|
+
or a release job that fails as a matter of course.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# bump `version` in pyproject.toml, note it in CHANGELOG.md, merge, then:
|
|
145
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The workflow builds wheels for linux and macOS on both architectures plus an sdist, checks
|
|
149
|
+
that the tag and the version in pyproject.toml agree, and only then uploads.
|
|
150
|
+
`workflow_dispatch` runs everything except the upload, which is how the pipeline is tested
|
|
151
|
+
without releasing anything.
|
|
152
|
+
|
|
153
|
+
The upload needs a credential, and there are two ways to give it one. Either works; the
|
|
154
|
+
first stores nothing.
|
|
155
|
+
|
|
156
|
+
**Trusted publishing.** On PyPI, add a pending publisher for the project name with owner
|
|
157
|
+
`Derfirm`, repository `lz-string`, workflow `release.yml`, environment `pypi`. Nothing to
|
|
158
|
+
configure here — with no `PYPI_API_TOKEN` secret set, the publish step already exchanges a
|
|
159
|
+
short-lived OIDC token, which is what it tries today and what fails with "Trusted publishing
|
|
160
|
+
exchange failure" until the publisher exists.
|
|
161
|
+
|
|
162
|
+
**A token.** Create an environment named `pypi` in the repository settings, put an API token
|
|
163
|
+
in it as `PYPI_API_TOKEN`, and re-run the publish job of the tagged release. Protection
|
|
164
|
+
rules on that environment are where you add an approval step if you want one.
|
|
165
|
+
|
|
166
|
+
## Provenance and licence
|
|
167
|
+
|
|
168
|
+
MIT (`LICENSE`). Compression comes from the [`lz-str`](https://crates.io/crates/lz-str)
|
|
169
|
+
crate and the bindings from [pyo3](https://pyo3.rs), both MIT/Apache-2.0; the decoder is a
|
|
170
|
+
port of [pieroxy/lz-string](https://github.com/pieroxy/lz-string), MIT, which is also the
|
|
171
|
+
reference every claim here is measured against. See `THIRD_PARTY.md`.
|
|
172
|
+
|
|
173
|
+
No user data lives in this repository: every test input is generated, and the corpus is the
|
|
174
|
+
reference implementation's own output.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
The Python and Rust code in this repository is MIT, see `LICENSE`. The compiled extension
|
|
4
|
+
links one crate, and its notice is reproduced as its licence requires.
|
|
5
|
+
|
|
6
|
+
## pyo3
|
|
7
|
+
|
|
8
|
+
<https://crates.io/crates/pyo3> — MIT OR Apache-2.0. The Python bindings.
|
|
9
|
+
|
|
10
|
+
## lz-string (JavaScript)
|
|
11
|
+
|
|
12
|
+
<https://github.com/pieroxy/lz-string> — MIT, by Pieroxy. Not linked or redistributed: it is
|
|
13
|
+
the *reference*. Both halves of the codec here are ports of it, its behaviour is what this
|
|
14
|
+
package is measured against, and the golden vectors in `tests/data/vectors.jsonl.gz` are its
|
|
15
|
+
output, produced by `tools/gen_vectors.mjs`. Everything this package claims about correctness
|
|
16
|
+
is a claim about agreeing with it.
|
|
17
|
+
|
|
18
|
+
## lz-str (Rust) — no longer a dependency
|
|
19
|
+
|
|
20
|
+
<https://crates.io/crates/lz-str> — MIT OR Apache-2.0. This package wrapped it until its
|
|
21
|
+
decompressor turned out to disagree with the reference on damaged input in three ways
|
|
22
|
+
(SPEC.md §7). Nothing of it ships here now; the note is kept because the analysis in SPEC.md
|
|
23
|
+
refers to it.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# 1.9 is the floor, not a guess: PEP 639 `license` / `license-files` below it are a
|
|
3
|
+
# parse error, and the build fails with "wanted string or table".
|
|
4
|
+
requires = ["maturin>=1.9,<2"]
|
|
5
|
+
build-backend = "maturin"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "lzstring-codec"
|
|
9
|
+
version = "0.1.0"
|
|
10
|
+
description = "lz-string for Python, held byte-for-byte to the JavaScript original"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE", "THIRD_PARTY.md"]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
authors = [{ name = "Andrew Grinevich", email = "beulea@gmail.com" }]
|
|
16
|
+
keywords = ["lz-string", "lzstring", "lzstring-codec", "compression", "lzw", "savegame", "utf-16"]
|
|
17
|
+
dependencies = []
|
|
18
|
+
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Rust",
|
|
26
|
+
"Topic :: System :: Archiving :: Compression",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Derfirm/lz-string"
|
|
32
|
+
Source = "https://github.com/Derfirm/lz-string"
|
|
33
|
+
Issues = "https://github.com/Derfirm/lz-string/issues"
|
|
34
|
+
Specification = "https://github.com/Derfirm/lz-string/blob/master/SPEC.md"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
38
|
+
|
|
39
|
+
# A mixed layout: Python under src/, the crate under rust/. maturin builds the extension
|
|
40
|
+
# straight into the package, so `pip install` yields something that works rather than half
|
|
41
|
+
# of something that does.
|
|
42
|
+
[tool.maturin]
|
|
43
|
+
python-source = "src"
|
|
44
|
+
module-name = "lz_string._native"
|
|
45
|
+
manifest-path = "rust/Cargo.toml"
|
|
46
|
+
features = ["pyo3/extension-module"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
pythonpath = ["src", "."]
|
|
51
|
+
addopts = "-q"
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 110
|
|
55
|
+
target-version = "py312"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "heck"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "libc"
|
|
13
|
+
version = "0.2.189"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "lz-string-native"
|
|
19
|
+
version = "0.1.0"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"pyo3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[[package]]
|
|
25
|
+
name = "once_cell"
|
|
26
|
+
version = "1.21.4"
|
|
27
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
28
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
29
|
+
|
|
30
|
+
[[package]]
|
|
31
|
+
name = "portable-atomic"
|
|
32
|
+
version = "1.15.0"
|
|
33
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
34
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
35
|
+
|
|
36
|
+
[[package]]
|
|
37
|
+
name = "proc-macro2"
|
|
38
|
+
version = "1.0.107"
|
|
39
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
40
|
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
41
|
+
dependencies = [
|
|
42
|
+
"unicode-ident",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[[package]]
|
|
46
|
+
name = "pyo3"
|
|
47
|
+
version = "0.29.2"
|
|
48
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
49
|
+
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
|
|
50
|
+
dependencies = [
|
|
51
|
+
"libc",
|
|
52
|
+
"once_cell",
|
|
53
|
+
"portable-atomic",
|
|
54
|
+
"pyo3-build-config",
|
|
55
|
+
"pyo3-ffi",
|
|
56
|
+
"pyo3-macros",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[[package]]
|
|
60
|
+
name = "pyo3-build-config"
|
|
61
|
+
version = "0.29.2"
|
|
62
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
63
|
+
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
|
|
64
|
+
dependencies = [
|
|
65
|
+
"target-lexicon",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[[package]]
|
|
69
|
+
name = "pyo3-ffi"
|
|
70
|
+
version = "0.29.2"
|
|
71
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
72
|
+
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
|
|
73
|
+
dependencies = [
|
|
74
|
+
"libc",
|
|
75
|
+
"pyo3-build-config",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[[package]]
|
|
79
|
+
name = "pyo3-macros"
|
|
80
|
+
version = "0.29.2"
|
|
81
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
82
|
+
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
|
|
83
|
+
dependencies = [
|
|
84
|
+
"proc-macro2",
|
|
85
|
+
"pyo3-macros-backend",
|
|
86
|
+
"quote",
|
|
87
|
+
"syn",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[[package]]
|
|
91
|
+
name = "pyo3-macros-backend"
|
|
92
|
+
version = "0.29.2"
|
|
93
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
94
|
+
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
|
|
95
|
+
dependencies = [
|
|
96
|
+
"heck",
|
|
97
|
+
"proc-macro2",
|
|
98
|
+
"quote",
|
|
99
|
+
"syn",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[[package]]
|
|
103
|
+
name = "quote"
|
|
104
|
+
version = "1.0.47"
|
|
105
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
106
|
+
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
|
107
|
+
dependencies = [
|
|
108
|
+
"proc-macro2",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[[package]]
|
|
112
|
+
name = "syn"
|
|
113
|
+
version = "2.0.119"
|
|
114
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
115
|
+
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
116
|
+
dependencies = [
|
|
117
|
+
"proc-macro2",
|
|
118
|
+
"quote",
|
|
119
|
+
"unicode-ident",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[[package]]
|
|
123
|
+
name = "target-lexicon"
|
|
124
|
+
version = "0.13.5"
|
|
125
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
126
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
127
|
+
|
|
128
|
+
[[package]]
|
|
129
|
+
name = "unicode-ident"
|
|
130
|
+
version = "1.0.24"
|
|
131
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
132
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "lz-string-native"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
name = "_native"
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
pyo3 = { version = "0.29", features = ["extension-module", "abi3-py312"] }
|
|
13
|
+
|
|
14
|
+
[profile.release]
|
|
15
|
+
opt-level = 3
|
|
16
|
+
lto = true
|