rlz4 0.1.1
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.
- checksums.yaml +7 -0
- data/Cargo.lock +298 -0
- data/Cargo.toml +9 -0
- data/LICENSE +21 -0
- data/README.md +101 -0
- data/ext/rlz4/Cargo.toml +16 -0
- data/ext/rlz4/extconf.rb +8 -0
- data/ext/rlz4/src/lib.rs +226 -0
- data/lib/rlz4/version.rb +5 -0
- data/lib/rlz4.rb +4 -0
- data/tmp/x86_64-linux/stage/Cargo.toml +9 -0
- data/tmp/x86_64-linux/stage/ext/rlz4/Cargo.toml +16 -0
- metadata +115 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cc8e8cccbcbf8fd18231558a8e9486e630dadd9287f6a424d3565790ec2b28a4
|
|
4
|
+
data.tar.gz: e99944e60c12edb16ced111597ee628274c5fee7f977d6d6f1a57ca2217bfefa
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f548b432680ad0e5c12ae606696049f3710be1d26211973103d0607ee9241af8d786a95c8eb12e4bf5b52230387a9873bfd524913236f7be3a04672c98130b2b
|
|
7
|
+
data.tar.gz: f745e2487bff140a571d18670b23b22b77acfcaa66174bfa72cb740efdc2c708905414c1fce3ce6e31fe634446c523c2e257cbcecddc74babdd956634f916a68
|
data/Cargo.lock
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "aho-corasick"
|
|
7
|
+
version = "1.1.4"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"memchr",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "bindgen"
|
|
16
|
+
version = "0.72.1"
|
|
17
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
+
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"bitflags",
|
|
21
|
+
"cexpr",
|
|
22
|
+
"clang-sys",
|
|
23
|
+
"itertools",
|
|
24
|
+
"proc-macro2",
|
|
25
|
+
"quote",
|
|
26
|
+
"regex",
|
|
27
|
+
"rustc-hash",
|
|
28
|
+
"shlex",
|
|
29
|
+
"syn",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[[package]]
|
|
33
|
+
name = "bitflags"
|
|
34
|
+
version = "2.11.0"
|
|
35
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
36
|
+
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
|
37
|
+
|
|
38
|
+
[[package]]
|
|
39
|
+
name = "cexpr"
|
|
40
|
+
version = "0.6.0"
|
|
41
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
42
|
+
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
|
43
|
+
dependencies = [
|
|
44
|
+
"nom",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[[package]]
|
|
48
|
+
name = "cfg-if"
|
|
49
|
+
version = "1.0.4"
|
|
50
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
51
|
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
52
|
+
|
|
53
|
+
[[package]]
|
|
54
|
+
name = "clang-sys"
|
|
55
|
+
version = "1.8.1"
|
|
56
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
57
|
+
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
|
58
|
+
dependencies = [
|
|
59
|
+
"glob",
|
|
60
|
+
"libc",
|
|
61
|
+
"libloading",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[[package]]
|
|
65
|
+
name = "either"
|
|
66
|
+
version = "1.15.0"
|
|
67
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
68
|
+
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|
69
|
+
|
|
70
|
+
[[package]]
|
|
71
|
+
name = "glob"
|
|
72
|
+
version = "0.3.3"
|
|
73
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
74
|
+
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
|
75
|
+
|
|
76
|
+
[[package]]
|
|
77
|
+
name = "itertools"
|
|
78
|
+
version = "0.13.0"
|
|
79
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
80
|
+
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
|
81
|
+
dependencies = [
|
|
82
|
+
"either",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[[package]]
|
|
86
|
+
name = "lazy_static"
|
|
87
|
+
version = "1.5.0"
|
|
88
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
89
|
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
90
|
+
|
|
91
|
+
[[package]]
|
|
92
|
+
name = "libc"
|
|
93
|
+
version = "0.2.184"
|
|
94
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
95
|
+
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
|
96
|
+
|
|
97
|
+
[[package]]
|
|
98
|
+
name = "libloading"
|
|
99
|
+
version = "0.8.9"
|
|
100
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
101
|
+
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
|
102
|
+
dependencies = [
|
|
103
|
+
"cfg-if",
|
|
104
|
+
"windows-link",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
[[package]]
|
|
108
|
+
name = "lz4_flex"
|
|
109
|
+
version = "0.13.0"
|
|
110
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
111
|
+
checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a"
|
|
112
|
+
dependencies = [
|
|
113
|
+
"twox-hash",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[[package]]
|
|
117
|
+
name = "magnus"
|
|
118
|
+
version = "0.8.2"
|
|
119
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
120
|
+
checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
|
|
121
|
+
dependencies = [
|
|
122
|
+
"magnus-macros",
|
|
123
|
+
"rb-sys",
|
|
124
|
+
"rb-sys-env",
|
|
125
|
+
"seq-macro",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
[[package]]
|
|
129
|
+
name = "magnus-macros"
|
|
130
|
+
version = "0.8.0"
|
|
131
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
132
|
+
checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
|
|
133
|
+
dependencies = [
|
|
134
|
+
"proc-macro2",
|
|
135
|
+
"quote",
|
|
136
|
+
"syn",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
[[package]]
|
|
140
|
+
name = "memchr"
|
|
141
|
+
version = "2.8.0"
|
|
142
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
143
|
+
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
144
|
+
|
|
145
|
+
[[package]]
|
|
146
|
+
name = "minimal-lexical"
|
|
147
|
+
version = "0.2.1"
|
|
148
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
149
|
+
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
150
|
+
|
|
151
|
+
[[package]]
|
|
152
|
+
name = "nom"
|
|
153
|
+
version = "7.1.3"
|
|
154
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
155
|
+
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
156
|
+
dependencies = [
|
|
157
|
+
"memchr",
|
|
158
|
+
"minimal-lexical",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
[[package]]
|
|
162
|
+
name = "proc-macro2"
|
|
163
|
+
version = "1.0.106"
|
|
164
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
165
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
166
|
+
dependencies = [
|
|
167
|
+
"unicode-ident",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
[[package]]
|
|
171
|
+
name = "quote"
|
|
172
|
+
version = "1.0.45"
|
|
173
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
174
|
+
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
|
175
|
+
dependencies = [
|
|
176
|
+
"proc-macro2",
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
[[package]]
|
|
180
|
+
name = "rb-sys"
|
|
181
|
+
version = "0.9.126"
|
|
182
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
183
|
+
checksum = "284799e73e899fe946fd77c7211b83bff61a1356e039ade7a2516a779e3212d0"
|
|
184
|
+
dependencies = [
|
|
185
|
+
"rb-sys-build",
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
[[package]]
|
|
189
|
+
name = "rb-sys-build"
|
|
190
|
+
version = "0.9.126"
|
|
191
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
192
|
+
checksum = "855fc1ad8943d12c89ef12f9147f1cc531f5bf19fb744112fdd317bb6ee7b5c5"
|
|
193
|
+
dependencies = [
|
|
194
|
+
"bindgen",
|
|
195
|
+
"lazy_static",
|
|
196
|
+
"proc-macro2",
|
|
197
|
+
"quote",
|
|
198
|
+
"regex",
|
|
199
|
+
"shell-words",
|
|
200
|
+
"syn",
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
[[package]]
|
|
204
|
+
name = "rb-sys-env"
|
|
205
|
+
version = "0.2.3"
|
|
206
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
207
|
+
checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
|
|
208
|
+
|
|
209
|
+
[[package]]
|
|
210
|
+
name = "regex"
|
|
211
|
+
version = "1.12.3"
|
|
212
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
213
|
+
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
|
214
|
+
dependencies = [
|
|
215
|
+
"aho-corasick",
|
|
216
|
+
"memchr",
|
|
217
|
+
"regex-automata",
|
|
218
|
+
"regex-syntax",
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
[[package]]
|
|
222
|
+
name = "regex-automata"
|
|
223
|
+
version = "0.4.14"
|
|
224
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
225
|
+
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
|
226
|
+
dependencies = [
|
|
227
|
+
"aho-corasick",
|
|
228
|
+
"memchr",
|
|
229
|
+
"regex-syntax",
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
[[package]]
|
|
233
|
+
name = "regex-syntax"
|
|
234
|
+
version = "0.8.10"
|
|
235
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
236
|
+
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
|
237
|
+
|
|
238
|
+
[[package]]
|
|
239
|
+
name = "rlz4"
|
|
240
|
+
version = "0.1.0"
|
|
241
|
+
dependencies = [
|
|
242
|
+
"lz4_flex",
|
|
243
|
+
"magnus",
|
|
244
|
+
"rb-sys",
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
[[package]]
|
|
248
|
+
name = "rustc-hash"
|
|
249
|
+
version = "2.1.2"
|
|
250
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
251
|
+
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
|
252
|
+
|
|
253
|
+
[[package]]
|
|
254
|
+
name = "seq-macro"
|
|
255
|
+
version = "0.3.6"
|
|
256
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
257
|
+
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
|
258
|
+
|
|
259
|
+
[[package]]
|
|
260
|
+
name = "shell-words"
|
|
261
|
+
version = "1.1.1"
|
|
262
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
263
|
+
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
|
264
|
+
|
|
265
|
+
[[package]]
|
|
266
|
+
name = "shlex"
|
|
267
|
+
version = "1.3.0"
|
|
268
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
269
|
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
270
|
+
|
|
271
|
+
[[package]]
|
|
272
|
+
name = "syn"
|
|
273
|
+
version = "2.0.117"
|
|
274
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
275
|
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
276
|
+
dependencies = [
|
|
277
|
+
"proc-macro2",
|
|
278
|
+
"quote",
|
|
279
|
+
"unicode-ident",
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
[[package]]
|
|
283
|
+
name = "twox-hash"
|
|
284
|
+
version = "2.1.2"
|
|
285
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
286
|
+
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
|
|
287
|
+
|
|
288
|
+
[[package]]
|
|
289
|
+
name = "unicode-ident"
|
|
290
|
+
version = "1.0.24"
|
|
291
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
292
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
293
|
+
|
|
294
|
+
[[package]]
|
|
295
|
+
name = "windows-link"
|
|
296
|
+
version = "0.2.1"
|
|
297
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
298
|
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
data/Cargo.toml
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrik Wenger
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# rlz4
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/rlz4)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](https://www.rust-lang.org)
|
|
7
|
+
|
|
8
|
+
Ractor-safe LZ4 bindings for Ruby, built as a Rust extension on top of
|
|
9
|
+
[`lz4_flex`](https://github.com/PSeitz/lz4_flex) via [`magnus`](https://github.com/matsadler/magnus).
|
|
10
|
+
|
|
11
|
+
## Why?
|
|
12
|
+
|
|
13
|
+
The existing Ruby LZ4 gems are broken under Ractor:
|
|
14
|
+
|
|
15
|
+
- [`lz4-ruby`](https://github.com/komiya-atsushi/lz4-ruby)
|
|
16
|
+
- [`lz4-flex-rb`](https://github.com/Shopify/lz4-flex-rb)
|
|
17
|
+
|
|
18
|
+
`rlz4` marks the extension Ractor-safe at load time and uses only owned,
|
|
19
|
+
thread-safe state, so it can be called from any Ractor.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# Gemfile
|
|
25
|
+
gem "rlz4"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Building requires a Rust toolchain (stable).
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Frame format (module functions)
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "rlz4"
|
|
36
|
+
|
|
37
|
+
compressed = RLZ4.compress("hello world" * 100)
|
|
38
|
+
decompressed = RLZ4.decompress(compressed)
|
|
39
|
+
|
|
40
|
+
# Wire format is standard LZ4 frame (magic number 04 22 4D 18),
|
|
41
|
+
# interoperable with any other LZ4 frame implementation.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Invalid input raises `RLZ4::DecompressError` (a `StandardError` subclass):
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
begin
|
|
48
|
+
RLZ4.decompress("not a valid lz4 frame")
|
|
49
|
+
rescue RLZ4::DecompressError => e
|
|
50
|
+
warn e.message
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Dictionary compression
|
|
55
|
+
|
|
56
|
+
For workloads where many small messages share a common prefix (e.g. ZMQ
|
|
57
|
+
messages with a fixed header), a shared dictionary massively improves the
|
|
58
|
+
compression ratio. `RLZ4::Dictionary` uses LZ4 **block** format with the
|
|
59
|
+
original size prepended — this is a different wire format from
|
|
60
|
+
`RLZ4.compress` and is not interoperable with it.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
dict = RLZ4::Dictionary.new("schema=v1 type=message field1=")
|
|
64
|
+
|
|
65
|
+
compressed = dict.compress("schema=v1 type=message field1=payload")
|
|
66
|
+
decompressed = dict.decompress(compressed)
|
|
67
|
+
|
|
68
|
+
dict.size # => 30
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`RLZ4::Dictionary` is immutable after construction and can be shared across
|
|
72
|
+
Ractors.
|
|
73
|
+
|
|
74
|
+
### Ractors
|
|
75
|
+
|
|
76
|
+
Both the module functions and `RLZ4::Dictionary` can be used from any
|
|
77
|
+
Ractor. Example from the test suite:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
ractors = 4.times.map do |i|
|
|
81
|
+
Ractor.new(i) do |idx|
|
|
82
|
+
pt = "ractor #{idx} payload " * 1000
|
|
83
|
+
1000.times do
|
|
84
|
+
ct = RLZ4.compress(pt)
|
|
85
|
+
raise "mismatch" unless RLZ4.decompress(ct) == pt
|
|
86
|
+
end
|
|
87
|
+
:ok
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
ractors.map(&:value) # => [:ok, :ok, :ok, :ok]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Non-goals
|
|
94
|
+
|
|
95
|
+
- High-compression mode (LZ4_HC).
|
|
96
|
+
- Streaming / chunked compression.
|
|
97
|
+
- Preservation of string encoding on decompress (output is always binary).
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
data/ext/rlz4/Cargo.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rlz4"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
name = "rlz4"
|
|
8
|
+
crate-type = ["cdylib", "rlib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
lz4_flex = { version = "0.13", default-features = false, features = ["frame", "std", "safe-encode", "safe-decode"] }
|
|
12
|
+
magnus = "0.8"
|
|
13
|
+
rb-sys = "0.9"
|
|
14
|
+
|
|
15
|
+
[build-dependencies]
|
|
16
|
+
rb-sys = "0.9"
|
data/ext/rlz4/extconf.rb
ADDED
data/ext/rlz4/src/lib.rs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
use magnus::{
|
|
2
|
+
exception::ExceptionClass,
|
|
3
|
+
function, method,
|
|
4
|
+
prelude::*,
|
|
5
|
+
r_string::RString,
|
|
6
|
+
value::Opaque,
|
|
7
|
+
Error, Ruby,
|
|
8
|
+
};
|
|
9
|
+
use std::io::{Read, Write};
|
|
10
|
+
use std::sync::OnceLock;
|
|
11
|
+
|
|
12
|
+
use lz4_flex::frame::{FrameDecoder, FrameEncoder};
|
|
13
|
+
|
|
14
|
+
const LZ4_FRAME_MAGIC: [u8; 4] = [0x04, 0x22, 0x4d, 0x18];
|
|
15
|
+
|
|
16
|
+
// Opaque<T> is Send+Sync and is designed for storing Ruby values in statics.
|
|
17
|
+
static DECOMPRESS_ERROR: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
|
|
18
|
+
|
|
19
|
+
fn decompress_error(ruby: &Ruby) -> ExceptionClass {
|
|
20
|
+
ruby.get_inner(
|
|
21
|
+
*DECOMPRESS_ERROR
|
|
22
|
+
.get()
|
|
23
|
+
.expect("DecompressError not initialized"),
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------- module functions: frame-format compress/decompress ----------
|
|
28
|
+
|
|
29
|
+
fn rlz4_compress(ruby: &Ruby, rb_input: RString) -> Result<RString, Error> {
|
|
30
|
+
// SAFETY: copy borrowed bytes into an owned Vec before any Ruby allocation.
|
|
31
|
+
let input: Vec<u8> = unsafe { rb_input.as_slice().to_vec() };
|
|
32
|
+
|
|
33
|
+
// Pre-size the output buffer. Frame overhead is ~19 bytes for the header
|
|
34
|
+
// plus up to ~4 bytes per block end-marker — 64 is a comfortable ceiling.
|
|
35
|
+
let upper = lz4_flex::block::get_maximum_output_size(input.len()) + 64;
|
|
36
|
+
let mut encoder = FrameEncoder::new(Vec::with_capacity(upper));
|
|
37
|
+
encoder.write_all(&input).map_err(|e| {
|
|
38
|
+
Error::new(
|
|
39
|
+
ruby.exception_runtime_error(),
|
|
40
|
+
format!("lz4 frame encoder write failed: {e}"),
|
|
41
|
+
)
|
|
42
|
+
})?;
|
|
43
|
+
let compressed = encoder.finish().map_err(|e| {
|
|
44
|
+
Error::new(
|
|
45
|
+
ruby.exception_runtime_error(),
|
|
46
|
+
format!("lz4 frame encoder finish failed: {e}"),
|
|
47
|
+
)
|
|
48
|
+
})?;
|
|
49
|
+
|
|
50
|
+
Ok(ruby.str_from_slice(&compressed))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn rlz4_decompress(ruby: &Ruby, rb_input: RString) -> Result<RString, Error> {
|
|
54
|
+
// SAFETY: copy borrowed bytes before any Ruby allocation.
|
|
55
|
+
let compressed: Vec<u8> = unsafe { rb_input.as_slice().to_vec() };
|
|
56
|
+
|
|
57
|
+
// Reject anything that isn't a well-formed frame up front. lz4_flex's
|
|
58
|
+
// FrameDecoder permissively returns Ok for zero-length input, which would
|
|
59
|
+
// quietly mask "sender forgot --compress" mistakes in omq-cli.
|
|
60
|
+
if compressed.len() < LZ4_FRAME_MAGIC.len() || compressed[..4] != LZ4_FRAME_MAGIC {
|
|
61
|
+
return Err(Error::new(
|
|
62
|
+
decompress_error(ruby),
|
|
63
|
+
"lz4 frame decode failed: bad magic (input is not an LZ4 frame)",
|
|
64
|
+
));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Decode into a local Vec first. If this fails, we never allocate a
|
|
68
|
+
// Ruby string — important for DoS-resistance against malformed input.
|
|
69
|
+
let mut decoder = FrameDecoder::new(&compressed[..]);
|
|
70
|
+
let mut out = Vec::new();
|
|
71
|
+
decoder.read_to_end(&mut out).map_err(|e| {
|
|
72
|
+
Error::new(
|
|
73
|
+
decompress_error(ruby),
|
|
74
|
+
format!("lz4 frame decode failed: {e}"),
|
|
75
|
+
)
|
|
76
|
+
})?;
|
|
77
|
+
|
|
78
|
+
Ok(ruby.str_from_slice(&out))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------- Dictionary: block-format compression with a shared dictionary ----------
|
|
82
|
+
//
|
|
83
|
+
// lz4_flex's frame format does not implement dictionary-based compression
|
|
84
|
+
// (FrameInfo::dict_id is metadata-only). For the small-ZMQ-message use case
|
|
85
|
+
// that motivates this class, block format with a prepended size is a better
|
|
86
|
+
// fit anyway: lower per-message overhead and direct dictionary support.
|
|
87
|
+
//
|
|
88
|
+
// Output is a raw LZ4 block with the original (uncompressed) size prepended
|
|
89
|
+
// as a little-endian u32, matching lz4_flex's `*_size_prepended` API.
|
|
90
|
+
#[magnus::wrap(class = "RLZ4::Dictionary", free_immediately, size)]
|
|
91
|
+
struct Dictionary {
|
|
92
|
+
bytes: Vec<u8>,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Safety: Dictionary is read-only after construction (just a byte buffer).
|
|
96
|
+
// No interior mutability, no references to thread-local data.
|
|
97
|
+
unsafe impl Send for Dictionary {}
|
|
98
|
+
unsafe impl Sync for Dictionary {}
|
|
99
|
+
|
|
100
|
+
fn dict_initialize(_ruby: &Ruby, rb_dict: RString) -> Result<Dictionary, Error> {
|
|
101
|
+
// SAFETY: copy bytes into an owned Vec before any Ruby allocation.
|
|
102
|
+
let bytes: Vec<u8> = unsafe { rb_dict.as_slice().to_vec() };
|
|
103
|
+
rb_dict.freeze();
|
|
104
|
+
Ok(Dictionary { bytes })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn dict_compress(ruby: &Ruby, rb_self: &Dictionary, rb_input: RString) -> Result<RString, Error> {
|
|
108
|
+
let input: Vec<u8> = unsafe { rb_input.as_slice().to_vec() };
|
|
109
|
+
let compressed = lz4_flex::block::compress_prepend_size_with_dict(&input, &rb_self.bytes);
|
|
110
|
+
Ok(ruby.str_from_slice(&compressed))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn dict_decompress(
|
|
114
|
+
ruby: &Ruby,
|
|
115
|
+
rb_self: &Dictionary,
|
|
116
|
+
rb_input: RString,
|
|
117
|
+
) -> Result<RString, Error> {
|
|
118
|
+
let compressed: Vec<u8> = unsafe { rb_input.as_slice().to_vec() };
|
|
119
|
+
let out = lz4_flex::block::decompress_size_prepended_with_dict(&compressed, &rb_self.bytes)
|
|
120
|
+
.map_err(|e| {
|
|
121
|
+
Error::new(
|
|
122
|
+
decompress_error(ruby),
|
|
123
|
+
format!("lz4 block decode failed: {e}"),
|
|
124
|
+
)
|
|
125
|
+
})?;
|
|
126
|
+
Ok(ruby.str_from_slice(&out))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn dict_size(rb_self: &Dictionary) -> usize {
|
|
130
|
+
rb_self.bytes.len()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------- module init ----------
|
|
134
|
+
|
|
135
|
+
#[magnus::init]
|
|
136
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
137
|
+
// Mark this extension as Ractor-safe. All our Rust code uses only
|
|
138
|
+
// stack/owned data, holds no globals aside from the Opaque exception
|
|
139
|
+
// class (which is Send+Sync by construction), and the Dictionary type
|
|
140
|
+
// is read-only after init, so it is safe to call from any Ractor.
|
|
141
|
+
unsafe { rb_sys::rb_ext_ractor_safe(true) };
|
|
142
|
+
|
|
143
|
+
let module = ruby.define_module("RLZ4")?;
|
|
144
|
+
|
|
145
|
+
let decompress_error_class =
|
|
146
|
+
module.define_error("DecompressError", ruby.exception_standard_error())?;
|
|
147
|
+
DECOMPRESS_ERROR
|
|
148
|
+
.set(Opaque::from(decompress_error_class))
|
|
149
|
+
.unwrap_or_else(|_| panic!("init called more than once"));
|
|
150
|
+
|
|
151
|
+
module.define_module_function("compress", function!(rlz4_compress, 1))?;
|
|
152
|
+
module.define_module_function("decompress", function!(rlz4_decompress, 1))?;
|
|
153
|
+
|
|
154
|
+
let dict_class = module.define_class("Dictionary", ruby.class_object())?;
|
|
155
|
+
dict_class.define_singleton_method("new", function!(dict_initialize, 1))?;
|
|
156
|
+
dict_class.define_method("compress", method!(dict_compress, 1))?;
|
|
157
|
+
dict_class.define_method("decompress", method!(dict_decompress, 1))?;
|
|
158
|
+
dict_class.define_method("size", method!(dict_size, 0))?;
|
|
159
|
+
|
|
160
|
+
Ok(())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[cfg(test)]
|
|
164
|
+
mod tests {
|
|
165
|
+
use super::*;
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn frame_round_trip() {
|
|
169
|
+
let data = b"the quick brown fox jumps over the lazy dog ".repeat(100);
|
|
170
|
+
let mut enc = FrameEncoder::new(Vec::new());
|
|
171
|
+
enc.write_all(&data).unwrap();
|
|
172
|
+
let ct = enc.finish().unwrap();
|
|
173
|
+
assert!(ct.len() < data.len(), "should compress repetitive input");
|
|
174
|
+
// Frame magic number
|
|
175
|
+
assert_eq!(&ct[..4], &[0x04, 0x22, 0x4d, 0x18]);
|
|
176
|
+
|
|
177
|
+
let mut dec = FrameDecoder::new(&ct[..]);
|
|
178
|
+
let mut out = Vec::new();
|
|
179
|
+
dec.read_to_end(&mut out).unwrap();
|
|
180
|
+
assert_eq!(out, data);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[test]
|
|
184
|
+
fn frame_empty_round_trip() {
|
|
185
|
+
let mut enc = FrameEncoder::new(Vec::new());
|
|
186
|
+
enc.write_all(b"").unwrap();
|
|
187
|
+
let ct = enc.finish().unwrap();
|
|
188
|
+
let mut dec = FrameDecoder::new(&ct[..]);
|
|
189
|
+
let mut out = Vec::new();
|
|
190
|
+
dec.read_to_end(&mut out).unwrap();
|
|
191
|
+
assert!(out.is_empty());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[test]
|
|
195
|
+
fn frame_garbage_fails() {
|
|
196
|
+
// A buffer that is long enough to look like a frame but has the
|
|
197
|
+
// wrong magic number must fail to decode.
|
|
198
|
+
let garbage = vec![0xFFu8; 32];
|
|
199
|
+
let mut dec = FrameDecoder::new(&garbage[..]);
|
|
200
|
+
let mut out = Vec::new();
|
|
201
|
+
assert!(dec.read_to_end(&mut out).is_err());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn block_dict_round_trip() {
|
|
206
|
+
let dict = b"JSON schema version 1 field ";
|
|
207
|
+
let msg = b"JSON schema version 1 field name=hello value=world";
|
|
208
|
+
let ct = lz4_flex::block::compress_prepend_size_with_dict(msg, dict);
|
|
209
|
+
let pt = lz4_flex::block::decompress_size_prepended_with_dict(&ct, dict).unwrap();
|
|
210
|
+
assert_eq!(pt, msg);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn block_dict_mismatch_fails_or_returns_wrong_data() {
|
|
215
|
+
// With a wrong dict, decode either errors out or returns wrong bytes.
|
|
216
|
+
// Either way it must not silently round-trip to the original.
|
|
217
|
+
let dict_a = b"common prefix AAA ";
|
|
218
|
+
let dict_b = b"common prefix BBB ";
|
|
219
|
+
let msg = b"common prefix AAA : the payload";
|
|
220
|
+
let ct = lz4_flex::block::compress_prepend_size_with_dict(msg, dict_a);
|
|
221
|
+
match lz4_flex::block::decompress_size_prepended_with_dict(&ct, dict_b) {
|
|
222
|
+
Ok(out) => assert_ne!(out, msg),
|
|
223
|
+
Err(_) => {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
data/lib/rlz4/version.rb
ADDED
data/lib/rlz4.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rlz4"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
name = "rlz4"
|
|
8
|
+
crate-type = ["cdylib", "rlib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
lz4_flex = { version = "0.13", default-features = false, features = ["frame", "std", "safe-encode", "safe-decode"] }
|
|
12
|
+
magnus = "0.8"
|
|
13
|
+
rb-sys = "0.9"
|
|
14
|
+
|
|
15
|
+
[build-dependencies]
|
|
16
|
+
rb-sys = "0.9"
|
metadata
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rlz4
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Patrik Wenger
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.9'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake-compiler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.2'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: minitest
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '5.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '5.0'
|
|
68
|
+
description: |
|
|
69
|
+
Ruby bindings (via Rust/magnus) for the lz4_flex LZ4 implementation.
|
|
70
|
+
Provides LZ4 frame-format compress/decompress at module level and a
|
|
71
|
+
stateful Dictionary class for block-format compression with a shared
|
|
72
|
+
dictionary. Designed to be safe to call from multiple Ractors, unlike
|
|
73
|
+
existing Ruby LZ4 gems.
|
|
74
|
+
email:
|
|
75
|
+
- paddor@protonmail.ch
|
|
76
|
+
executables: []
|
|
77
|
+
extensions:
|
|
78
|
+
- ext/rlz4/extconf.rb
|
|
79
|
+
extra_rdoc_files: []
|
|
80
|
+
files:
|
|
81
|
+
- Cargo.lock
|
|
82
|
+
- Cargo.toml
|
|
83
|
+
- LICENSE
|
|
84
|
+
- README.md
|
|
85
|
+
- ext/rlz4/Cargo.toml
|
|
86
|
+
- ext/rlz4/extconf.rb
|
|
87
|
+
- ext/rlz4/src/lib.rs
|
|
88
|
+
- lib/rlz4.rb
|
|
89
|
+
- lib/rlz4/version.rb
|
|
90
|
+
- tmp/x86_64-linux/stage/Cargo.toml
|
|
91
|
+
- tmp/x86_64-linux/stage/ext/rlz4/Cargo.toml
|
|
92
|
+
homepage: https://github.com/paddor/rlz4
|
|
93
|
+
licenses:
|
|
94
|
+
- MIT
|
|
95
|
+
metadata:
|
|
96
|
+
homepage_uri: https://github.com/paddor/rlz4
|
|
97
|
+
source_code_uri: https://github.com/paddor/rlz4
|
|
98
|
+
rdoc_options: []
|
|
99
|
+
require_paths:
|
|
100
|
+
- lib
|
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: 4.0.0
|
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
requirements: []
|
|
112
|
+
rubygems_version: 4.0.6
|
|
113
|
+
specification_version: 4
|
|
114
|
+
summary: Ractor-safe LZ4 bindings for Ruby (Rust extension via lz4_flex)
|
|
115
|
+
test_files: []
|