vibe_zstd 1.3.0 → 1.3.2
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/ext/vibe_zstd/cctx.c +8 -9
- data/ext/vibe_zstd/dctx.c +18 -15
- data/ext/vibe_zstd/streaming.c +14 -43
- data/ext/vibe_zstd/vibe_zstd.c +26 -27
- data/lib/vibe_zstd/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e06ac31dfbfb1db79c69375a831303710877a6c07564c687a8f9cb394454060
|
|
4
|
+
data.tar.gz: 269506fc73069dca0dec661f8eca5beadafe397f0dfd8209bca310b6930eedaa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b836f11bfaae0728bf417f30b6b1b91f90a573b6ad052d7a58cb00644d6510575034ee3e27dfc6d65714e85ee6045f95a96e8303a0f3e5a45b9102b1702a3a45
|
|
7
|
+
data.tar.gz: 7e474b085f2e2614ec6fc23ec708e0f0796b319b8dfed10a7fb79099aa04fc0b2f3e56dd3a97a66cf3b3dbf77cecaf334b79bd4b475f3f126c16650783adc275
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.2] - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Chilled string literals (Ruby 3.4+, files without `# frozen_string_literal`) no longer trigger "literal string will be frozen in the future" warnings when passed to `CCtx#compress`, `DCtx#decompress`, `VibeZstd.compress`/`.decompress`, or `CompressWriter#write`. 1.3.1 still called `rb_str_locktmp` on them, which Ruby counts as a write.
|
|
14
|
+
- `DCtx#decompress` now snapshots its input before parsing options, so a keyword argument whose `to_int` mutates the input string can no longer leave a stale source pointer.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Source strings are no longer locked while the GVL is released. They are read from a frozen copy-on-write snapshot (`rb_str_new_frozen`) instead, the same approach `DecompressReader` already used for input chunks. Another thread (or an IO's `write`) mutating the caller's string mid-operation now succeeds, and the operation keeps reading the original bytes, instead of raising `RuntimeError` ("can't modify string; temporarily locked"). Likewise, a string another thread is concurrently reading into (`IO#read(n, buf)`) is no longer rejected; such a race now yields whatever bytes were in the buffer. Cost: the caller's string stays shared with the snapshot until its next mutation, so a buffer that is refilled and compressed in a loop (`io.read(n, buf); compress(buf)`) pays one copy of its contents per iteration. Fresh strings are unaffected.
|
|
18
|
+
|
|
19
|
+
## [1.3.1] - 2026-10-26
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Ruby 4.0 compatibility: `CCtx#compress`, `DCtx#decompress`, `VibeZstd.compress`/`.decompress`, and `CompressWriter#write` raised `FrozenError` when given a frozen string (including `# frozen_string_literal: true` literals). Ruby 4.0 refuses `rb_str_locktmp` on frozen strings; frozen input is already immutable, so the GVL-release lock added in 1.3.0 is now skipped for it. Unfrozen strings are still locked as before.
|
|
23
|
+
|
|
10
24
|
## [1.3.0] - 2026-06-11
|
|
11
25
|
|
|
12
26
|
### Security
|
data/ext/vibe_zstd/cctx.c
CHANGED
|
@@ -158,24 +158,23 @@ vibe_zstd_cctx_compress(int argc, VALUE* argv, VALUE self) {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
// Read the source from a frozen snapshot (see vibe_zstd_source_snapshot):
|
|
162
|
+
// another Ruby thread mutating the caller's string cannot invalidate
|
|
163
|
+
// args.src while the GVL is released.
|
|
164
|
+
VALUE src_str = vibe_zstd_source_snapshot(data);
|
|
165
|
+
size_t srcSize = RSTRING_LEN(src_str);
|
|
162
166
|
size_t dstCapacity = ZSTD_compressBound(srcSize);
|
|
163
167
|
VALUE result_str = rb_str_new(NULL, dstCapacity);
|
|
164
168
|
compress_args args = {
|
|
165
169
|
.cctx = cctx->cctx,
|
|
166
|
-
.src = RSTRING_PTR(
|
|
170
|
+
.src = RSTRING_PTR(src_str),
|
|
167
171
|
.srcSize = srcSize,
|
|
168
172
|
.dst = RSTRING_PTR(result_str),
|
|
169
173
|
.dstCapacity = dstCapacity,
|
|
170
174
|
.result = 0
|
|
171
175
|
};
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// modify or reallocate it while compression reads from its buffer, causing
|
|
175
|
-
// a use-after-free read. The helper unlocks via rb_ensure so the string
|
|
176
|
-
// is never left permanently locked, even if an async exception (e.g.
|
|
177
|
-
// Timeout, Thread#raise) is delivered when the GVL is reacquired.
|
|
178
|
-
vibe_zstd_nogvl_with_str_locked(compress_without_gvl, &args, data);
|
|
176
|
+
rb_thread_call_without_gvl(compress_without_gvl, &args, NULL, NULL);
|
|
177
|
+
RB_GC_GUARD(src_str);
|
|
179
178
|
|
|
180
179
|
// Restore context state so repeated one-shot calls remain independent.
|
|
181
180
|
if (cdict) ZSTD_CCtx_refCDict(cctx->cctx, NULL);
|
data/ext/vibe_zstd/dctx.c
CHANGED
|
@@ -411,19 +411,17 @@ typedef struct {
|
|
|
411
411
|
ZSTD_DCtx* dctx;
|
|
412
412
|
ZSTD_DDict* ddict;
|
|
413
413
|
decompress_stream_nogvl_args* args;
|
|
414
|
-
VALUE
|
|
414
|
+
VALUE src_str; // frozen snapshot of the source (vibe_zstd_source_snapshot)
|
|
415
415
|
size_t max_size;
|
|
416
416
|
} dctx_stream_decompress_state;
|
|
417
417
|
|
|
418
|
-
// Body: run the no-GVL stream loop (source
|
|
419
|
-
// and build the result string. Raising here is safe: cleanup always runs.
|
|
418
|
+
// Body: run the no-GVL stream loop (source is a frozen snapshot), check the
|
|
419
|
+
// outcome, and build the result string. Raising here is safe: cleanup always runs.
|
|
420
420
|
static VALUE
|
|
421
421
|
vibe_zstd_dctx_stream_decompress_body(VALUE p) {
|
|
422
422
|
dctx_stream_decompress_state* state = (dctx_stream_decompress_state*)p;
|
|
423
423
|
|
|
424
|
-
|
|
425
|
-
// holding the same string must not mutate or GC it mid-decompression.
|
|
426
|
-
vibe_zstd_nogvl_with_str_locked(decompress_stream_without_gvl, state->args, state->data);
|
|
424
|
+
rb_thread_call_without_gvl(decompress_stream_without_gvl, state->args, NULL, NULL);
|
|
427
425
|
|
|
428
426
|
if (state->args->limit_exceeded) {
|
|
429
427
|
rb_raise(rb_eDecompressedSizeExceeded,
|
|
@@ -443,10 +441,12 @@ vibe_zstd_dctx_stream_decompress_body(VALUE p) {
|
|
|
443
441
|
}
|
|
444
442
|
|
|
445
443
|
// Cleanup: free the C output buffer and return the context to no-dictionary
|
|
446
|
-
// mode so subsequent calls on this DCtx are not affected.
|
|
444
|
+
// mode so subsequent calls on this DCtx are not affected. The snapshot is
|
|
445
|
+
// guarded here so it outlives every read of it in the body.
|
|
447
446
|
static VALUE
|
|
448
447
|
vibe_zstd_dctx_stream_decompress_cleanup(VALUE p) {
|
|
449
448
|
dctx_stream_decompress_state* state = (dctx_stream_decompress_state*)p;
|
|
449
|
+
RB_GC_GUARD(state->src_str);
|
|
450
450
|
if (state->args->dst) {
|
|
451
451
|
free(state->args->dst);
|
|
452
452
|
state->args->dst = NULL;
|
|
@@ -493,8 +493,12 @@ vibe_zstd_dctx_decompress(int argc, VALUE* argv, VALUE self) {
|
|
|
493
493
|
vibe_zstd_dctx* dctx;
|
|
494
494
|
TypedData_Get_Struct(self, vibe_zstd_dctx, &vibe_zstd_dctx_type, dctx);
|
|
495
495
|
StringValue(data);
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
// Snapshot the source before taking src: option parsing below can run
|
|
497
|
+
// Ruby code (to_int), and the GVL is released during decompression
|
|
498
|
+
// (see vibe_zstd_source_snapshot).
|
|
499
|
+
VALUE src_str = vibe_zstd_source_snapshot(data);
|
|
500
|
+
const char* src = RSTRING_PTR(src_str);
|
|
501
|
+
size_t srcSize = RSTRING_LEN(src_str);
|
|
498
502
|
size_t offset = 0;
|
|
499
503
|
|
|
500
504
|
// Magicless frames (format = ZSTD_f_zstd1_magicless) carry no magic number,
|
|
@@ -642,7 +646,7 @@ vibe_zstd_dctx_decompress(int argc, VALUE* argv, VALUE self) {
|
|
|
642
646
|
.dctx = dctx->dctx,
|
|
643
647
|
.ddict = ddict,
|
|
644
648
|
.args = &stream_args,
|
|
645
|
-
.
|
|
649
|
+
.src_str = src_str,
|
|
646
650
|
.max_size = max_size
|
|
647
651
|
};
|
|
648
652
|
return rb_ensure(vibe_zstd_dctx_stream_decompress_body, (VALUE)&state,
|
|
@@ -665,11 +669,10 @@ vibe_zstd_dctx_decompress(int argc, VALUE* argv, VALUE self) {
|
|
|
665
669
|
.dstCapacity = contentSize,
|
|
666
670
|
.result = 0
|
|
667
671
|
};
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
vibe_zstd_nogvl_with_str_locked(decompress_without_gvl, &args, data);
|
|
672
|
+
// src points into the frozen snapshot src_str, so it stays valid while
|
|
673
|
+
// the GVL is released even if the caller's string is mutated.
|
|
674
|
+
rb_thread_call_without_gvl(decompress_without_gvl, &args, NULL, NULL);
|
|
675
|
+
RB_GC_GUARD(src_str);
|
|
673
676
|
if (ZSTD_isError(args.result)) {
|
|
674
677
|
rb_raise(rb_eRuntimeError, "Decompression failed: %s", ZSTD_getErrorName(args.result));
|
|
675
678
|
}
|
data/ext/vibe_zstd/streaming.c
CHANGED
|
@@ -14,12 +14,6 @@ static VALUE vibe_zstd_reader_initialize(int argc, VALUE *argv, VALUE self);
|
|
|
14
14
|
static VALUE vibe_zstd_reader_read(int argc, VALUE *argv, VALUE self);
|
|
15
15
|
static VALUE vibe_zstd_reader_eof(VALUE self);
|
|
16
16
|
|
|
17
|
-
// State struct for rb_ensure-based string lock/unlock in vibe_zstd_writer_write
|
|
18
|
-
typedef struct {
|
|
19
|
-
vibe_zstd_cstream* cstream;
|
|
20
|
-
VALUE data;
|
|
21
|
-
} vibe_zstd_write_state;
|
|
22
|
-
|
|
23
17
|
// TypedData types - defined in vibe_zstd.c
|
|
24
18
|
extern rb_data_type_t vibe_zstd_cstream_type;
|
|
25
19
|
extern rb_data_type_t vibe_zstd_dstream_type;
|
|
@@ -106,19 +100,22 @@ vibe_zstd_writer_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
106
100
|
return self;
|
|
107
101
|
}
|
|
108
102
|
|
|
109
|
-
// Body of the rb_ensure wrapper: runs the compress loop with data locked
|
|
110
103
|
static VALUE
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
vibe_zstd_writer_write(VALUE self, VALUE data) {
|
|
105
|
+
Check_Type(data, T_STRING);
|
|
106
|
+
|
|
107
|
+
vibe_zstd_cstream* cstream;
|
|
108
|
+
TypedData_Get_Struct(self, vibe_zstd_cstream, &vibe_zstd_cstream_type, cstream);
|
|
109
|
+
|
|
110
|
+
// io.write (called inside the loop) can run Ruby code that mutates or
|
|
111
|
+
// resizes the caller's string. Reading from a frozen snapshot keeps
|
|
112
|
+
// input.src valid regardless (see vibe_zstd_source_snapshot).
|
|
113
|
+
VALUE src_str = vibe_zstd_source_snapshot(data);
|
|
115
114
|
|
|
116
115
|
// Input buffer: pos advances as ZSTD consumes data.
|
|
117
|
-
// data is locked (rb_str_locktmp) for the duration of this call so that
|
|
118
|
-
// RSTRING_PTR remains valid even when rb_funcall runs arbitrary Ruby code.
|
|
119
116
|
ZSTD_inBuffer input = {
|
|
120
|
-
.src = RSTRING_PTR(
|
|
121
|
-
.size = RSTRING_LEN(
|
|
117
|
+
.src = RSTRING_PTR(src_str),
|
|
118
|
+
.size = RSTRING_LEN(src_str),
|
|
122
119
|
.pos = 0
|
|
123
120
|
};
|
|
124
121
|
|
|
@@ -149,38 +146,12 @@ vibe_zstd_writer_write_body(VALUE arg) {
|
|
|
149
146
|
if (output.pos > 0) {
|
|
150
147
|
rb_str_set_len(outBuffer, output.pos);
|
|
151
148
|
// rb_funcall may run arbitrary Ruby code, but input.src stays valid
|
|
152
|
-
// because
|
|
149
|
+
// because it points into the frozen snapshot src_str
|
|
153
150
|
rb_funcall(cstream->io, id_write, 1, outBuffer);
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Ensure function: always unlocks data regardless of raise/return
|
|
161
|
-
static VALUE
|
|
162
|
-
vibe_zstd_writer_write_unlock(VALUE arg) {
|
|
163
|
-
rb_str_unlocktmp((VALUE)arg);
|
|
164
|
-
return Qnil;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
static VALUE
|
|
168
|
-
vibe_zstd_writer_write(VALUE self, VALUE data) {
|
|
169
|
-
Check_Type(data, T_STRING);
|
|
170
|
-
|
|
171
|
-
vibe_zstd_cstream* cstream;
|
|
172
|
-
TypedData_Get_Struct(self, vibe_zstd_cstream, &vibe_zstd_cstream_type, cstream);
|
|
173
|
-
|
|
174
|
-
// Lock data for the duration of the compress loop so that RSTRING_PTR(data)
|
|
175
|
-
// stays valid even when io.write (called inside the loop) runs Ruby code that
|
|
176
|
-
// could otherwise mutate or resize the string. rb_str_locktmp raises if the
|
|
177
|
-
// string is already locked; the ensure always unlocks it.
|
|
178
|
-
rb_str_locktmp(data);
|
|
179
|
-
|
|
180
|
-
vibe_zstd_write_state state = { cstream, data };
|
|
181
|
-
rb_ensure(vibe_zstd_writer_write_body, (VALUE)&state,
|
|
182
|
-
vibe_zstd_writer_write_unlock, data);
|
|
183
|
-
|
|
154
|
+
RB_GC_GUARD(src_str);
|
|
184
155
|
return self;
|
|
185
156
|
}
|
|
186
157
|
|
data/ext/vibe_zstd/vibe_zstd.c
CHANGED
|
@@ -280,33 +280,32 @@ vibe_zstd_default_c_level(VALUE self) {
|
|
|
280
280
|
return INT2NUM(ZSTD_defaultCLevel());
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
rb_ensure(nogvl_locked_body, (VALUE)&call, nogvl_locked_unlock, str);
|
|
283
|
+
// Source strings are read while the GVL is released (compress/decompress) or
|
|
284
|
+
// while arbitrary Ruby code runs (CompressWriter#write calling io.write), so
|
|
285
|
+
// the caller's String must not be mutated, resized or freed under us.
|
|
286
|
+
// rb_str_locktmp did that through 1.3.1, but Ruby 4.0 raises FrozenError on
|
|
287
|
+
// frozen strings and treats the lock as a write on chilled string literals
|
|
288
|
+
// ("literal string will be frozen in the future"). Instead, borrow the bytes
|
|
289
|
+
// through a frozen snapshot that shares the caller's buffer copy-on-write: a
|
|
290
|
+
// mutation unshares the caller's string and the snapshot keeps the original.
|
|
291
|
+
//
|
|
292
|
+
// The snapshot is a visible frozen String (rb_str_new_frozen) that owns the
|
|
293
|
+
// buffer for as long as anything references it, so a dup/clone of the
|
|
294
|
+
// caller's string taken mid-operation simply shares the same root. The
|
|
295
|
+
// caller's string stays copy-on-write shared until its next mutation, which
|
|
296
|
+
// then costs one copy; that is the price of this approach.
|
|
297
|
+
//
|
|
298
|
+
// Do NOT "optimize" this with rb_str_tmp_frozen_acquire/release (the IO#write
|
|
299
|
+
// primitive) to hand the buffer back afterwards: on Ruby 3.3+ String#dup of
|
|
300
|
+
// a string sharing a hidden root does not mark the root as borrowed, so
|
|
301
|
+
// release un-shares the caller's string and empties the root while the dup
|
|
302
|
+
// still points into the buffer. Any dup taken in io.write, or from another
|
|
303
|
+
// thread during compression, then aliases freed memory (reproduced).
|
|
304
|
+
//
|
|
305
|
+
// Callers must RB_GC_GUARD the snapshot after their last read of it.
|
|
306
|
+
static inline VALUE
|
|
307
|
+
vibe_zstd_source_snapshot(VALUE str) {
|
|
308
|
+
return rb_str_new_frozen(str);
|
|
310
309
|
}
|
|
311
310
|
|
|
312
311
|
// Include the split implementation files
|
data/lib/vibe_zstd/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vibe_zstd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kelley Reynolds
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: benchmark-ips
|