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 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
@@ -0,0 +1,9 @@
1
+ [workspace]
2
+ members = ["ext/rlz4"]
3
+ resolver = "2"
4
+
5
+ [profile.release]
6
+ opt-level = 3
7
+ lto = true
8
+ codegen-units = 1
9
+ panic = "abort"
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
+ [![Gem Version](https://img.shields.io/gem/v/rlz4?color=e9573f)](https://rubygems.org/gems/rlz4)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%204.0-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
6
+ [![Rust](https://img.shields.io/badge/Rust-stable-dea584?logo=rust&logoColor=white)](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
@@ -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"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("rlz4/rlz4") do |r|
7
+ r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :release).to_sym
8
+ end
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLZ4
4
+ VERSION = "0.1.1"
5
+ end
data/lib/rlz4.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rlz4/rlz4"
4
+ require_relative "rlz4/version"
@@ -0,0 +1,9 @@
1
+ [workspace]
2
+ members = ["ext/rlz4"]
3
+ resolver = "2"
4
+
5
+ [profile.release]
6
+ opt-level = 3
7
+ lto = true
8
+ codegen-units = 1
9
+ panic = "abort"
@@ -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: []