Skip to content

Commit ed5887a

Browse files
committed
Merge branch 'main' of github.com:SpringMT/zstd-ruby
2 parents d07c1c6 + 39894ff commit ed5887a

8 files changed

Lines changed: 92 additions & 5 deletions

‎ext/zstdruby/common.h‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ static int convert_compression_level(ZSTD_CCtx* ctx, VALUE compression_level_val
2424
return NUM2INT(compression_level_value);
2525
}
2626

27-
static void set_compress_params(ZSTD_CCtx* const ctx, VALUE kwargs)
27+
/* Returns the Zstd::CDict given as `dict:`, or Qnil. ZSTD_CCtx_refCDict only
28+
borrows the pointer, so a caller that keeps the ZSTD_CCtx alive beyond this
29+
call has to keep the returned object reachable for just as long. A String
30+
dictionary needs no such handling: ZSTD_CCtx_loadDictionary copies it. */
31+
static VALUE set_compress_params(ZSTD_CCtx* const ctx, VALUE kwargs)
2832
{
2933
ID kwargs_keys[2];
3034
kwargs_keys[0] = rb_intern("level");
@@ -46,6 +50,7 @@ static void set_compress_params(ZSTD_CCtx* const ctx, VALUE kwargs)
4650
ZSTD_freeCCtx(ctx);
4751
rb_raise(rb_eRuntimeError, "%s", "ZSTD_CCtx_refCDict failed");
4852
}
53+
return kwargs_values[1];
4954
} else if (TYPE(kwargs_values[1]) == T_STRING) {
5055
char* dict_buffer = RSTRING_PTR(kwargs_values[1]);
5156
size_t dict_size = RSTRING_LEN(kwargs_values[1]);
@@ -59,6 +64,7 @@ static void set_compress_params(ZSTD_CCtx* const ctx, VALUE kwargs)
5964
rb_raise(rb_eArgError, "`dict:` must be a Zstd::CDict or a String");
6065
}
6166
}
67+
return Qnil;
6268
}
6369

6470
struct stream_compress_params {
@@ -122,7 +128,9 @@ static size_t zstd_compress(ZSTD_CCtx* const ctx, char* output_data, size_t outp
122128
#endif
123129
}
124130

125-
static void set_decompress_params(ZSTD_DCtx* const dctx, VALUE kwargs)
131+
/* Returns the Zstd::DDict given as `dict:`, or Qnil. See set_compress_params:
132+
ZSTD_DCtx_refDDict borrows, ZSTD_DCtx_loadDictionary copies. */
133+
static VALUE set_decompress_params(ZSTD_DCtx* const dctx, VALUE kwargs)
126134
{
127135
ID kwargs_keys[1];
128136
kwargs_keys[0] = rb_intern("dict");
@@ -137,6 +145,7 @@ static void set_decompress_params(ZSTD_DCtx* const dctx, VALUE kwargs)
137145
ZSTD_freeDCtx(dctx);
138146
rb_raise(rb_eRuntimeError, "%s", "ZSTD_DCtx_refDDict failed");
139147
}
148+
return kwargs_values[0];
140149
} else if (TYPE(kwargs_values[0]) == T_STRING) {
141150
char* dict_buffer = RSTRING_PTR(kwargs_values[0]);
142151
size_t dict_size = RSTRING_LEN(kwargs_values[0]);
@@ -150,6 +159,7 @@ static void set_decompress_params(ZSTD_DCtx* const dctx, VALUE kwargs)
150159
rb_raise(rb_eArgError, "`dict:` must be a Zstd::DDict or a String");
151160
}
152161
}
162+
return Qnil;
153163
}
154164

155165
struct stream_decompress_params {

‎ext/zstdruby/extconf.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def ext_export_filename
2626
name
2727
end
2828

29-
$CFLAGS = '-I. -O3 -std=c99 -DZSTD_STATIC_LINKING_ONLY -DZSTD_MULTITHREAD -pthread -DDEBUGLEVEL=0 -fvisibility=hidden -DZSTDLIB_VISIBLE=\'__attribute__((visibility("hidden")))\' -DZSTDLIB_HIDDEN=\'__attribute__((visibility("hidden")))\''
29+
$CFLAGS += ' -I. -O3 -std=c99 -DZSTD_STATIC_LINKING_ONLY -DZSTD_MULTITHREAD -pthread -DDEBUGLEVEL=0 -fvisibility=hidden -DZSTDLIB_VISIBLE=\'__attribute__((visibility("hidden")))\' -DZSTDLIB_HIDDEN=\'__attribute__((visibility("hidden")))\''
3030
$CPPFLAGS += " -fdeclspec" if CONFIG['CXX'] =~ /clang/
3131

3232
# macOS specific: Use exported_symbols_list to control symbol visibility

‎ext/zstdruby/streaming_compress.c‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ struct streaming_compress_t {
55
VALUE buf;
66
size_t buf_size;
77
VALUE pending; /* accumulate compressed bytes produced by write() */
8+
VALUE dict; /* Zstd::CDict the ctx borrows a pointer into, or Qnil */
89
};
910

1011
static void
@@ -14,9 +15,11 @@ streaming_compress_mark(void *p)
1415
#ifdef HAVE_RB_GC_MARK_MOVABLE
1516
rb_gc_mark_movable(sc->buf);
1617
rb_gc_mark_movable(sc->pending);
18+
rb_gc_mark_movable(sc->dict);
1719
#else
1820
rb_gc_mark(sc->buf);
1921
rb_gc_mark(sc->pending);
22+
rb_gc_mark(sc->dict);
2023
#endif
2124
}
2225

@@ -44,6 +47,7 @@ streaming_compress_compact(void *p)
4447
struct streaming_compress_t *sc = p;
4548
sc->buf = rb_gc_location(sc->buf);
4649
sc->pending = rb_gc_location(sc->pending);
50+
sc->dict = rb_gc_location(sc->dict);
4751
}
4852
#endif
4953

@@ -69,6 +73,7 @@ rb_streaming_compress_allocate(VALUE klass)
6973
RB_OBJ_WRITE(obj, &sc->buf, Qnil);
7074
sc->buf_size = 0;
7175
RB_OBJ_WRITE(obj, &sc->pending, Qnil);
76+
RB_OBJ_WRITE(obj, &sc->dict, Qnil);
7277
return obj;
7378
}
7479

@@ -86,9 +91,10 @@ rb_streaming_compress_initialize(int argc, VALUE *argv, VALUE obj)
8691
if (ctx == NULL) {
8792
rb_raise(rb_eRuntimeError, "%s", "ZSTD_createCCtx error");
8893
}
89-
set_compress_params(ctx, kwargs);
94+
VALUE dict = set_compress_params(ctx, kwargs);
9095

9196
sc->ctx = ctx;
97+
RB_OBJ_WRITE(obj, &sc->dict, dict);
9298
RB_OBJ_WRITE(obj, &sc->buf, rb_str_new(NULL, buffOutSize));
9399
sc->buf_size = buffOutSize;
94100
RB_OBJ_WRITE(obj, &sc->pending, rb_str_new(0, 0));

‎ext/zstdruby/streaming_decompress.c‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ struct streaming_decompress_t {
44
ZSTD_DCtx* dctx;
55
VALUE buf;
66
size_t buf_size;
7+
VALUE dict; /* Zstd::DDict the dctx borrows a pointer into, or Qnil */
78
};
89

910
static void
@@ -12,8 +13,10 @@ streaming_decompress_mark(void *p)
1213
struct streaming_decompress_t *sd = p;
1314
#ifdef HAVE_RB_GC_MARK_MOVABLE
1415
rb_gc_mark_movable(sd->buf);
16+
rb_gc_mark_movable(sd->dict);
1517
#else
1618
rb_gc_mark(sd->buf);
19+
rb_gc_mark(sd->dict);
1720
#endif
1821
}
1922

@@ -40,6 +43,7 @@ streaming_decompress_compact(void *p)
4043
{
4144
struct streaming_decompress_t *sd = p;
4245
sd->buf = rb_gc_location(sd->buf);
46+
sd->dict = rb_gc_location(sd->dict);
4347
}
4448
#endif
4549

@@ -64,6 +68,7 @@ rb_streaming_decompress_allocate(VALUE klass)
6468
sd->dctx = NULL;
6569
RB_OBJ_WRITE(obj, &sd->buf, Qnil);
6670
sd->buf_size = 0;
71+
RB_OBJ_WRITE(obj, &sd->dict, Qnil);
6772
return obj;
6873
}
6974

@@ -81,9 +86,10 @@ rb_streaming_decompress_initialize(int argc, VALUE *argv, VALUE obj)
8186
if (dctx == NULL) {
8287
rb_raise(rb_eRuntimeError, "%s", "ZSTD_createDCtx error");
8388
}
84-
set_decompress_params(dctx, kwargs);
89+
VALUE dict = set_decompress_params(dctx, kwargs);
8590

8691
sd->dctx = dctx;
92+
RB_OBJ_WRITE(obj, &sd->dict, dict);
8793
RB_OBJ_WRITE(obj, &sd->buf, rb_str_new(NULL, buffOutSize));
8894
sd->buf_size = buffOutSize;
8995

‎ext/zstdruby/zstdruby.c‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ static VALUE decode_one_frame(ZSTD_DCtx* dctx, const unsigned char* src, size_t
5151

5252
for (;;) {
5353
ZSTD_outBuffer o = (ZSTD_outBuffer){ buf, cap, 0 };
54+
size_t const in_pos_before = in.pos;
5455
size_t ret = ZSTD_decompressStream(dctx, &o, &in);
5556
if (ZSTD_isError(ret)) {
5657
xfree(buf);
@@ -62,6 +63,13 @@ static VALUE decode_one_frame(ZSTD_DCtx* dctx, const unsigned char* src, size_t
6263
if (ret == 0) {
6364
break;
6465
}
66+
/* A non-zero return is a "need more input" hint, not an error, and libzstd's
67+
own noForwardProgress guard is bypassed by the early return it takes on a
68+
truncated frame header -- so the stall has to be detected here. */
69+
if (o.pos == 0 && in.pos == in_pos_before) {
70+
xfree(buf);
71+
rb_raise(rb_eRuntimeError, "ZSTD_decompressStream failed: truncated or incomplete frame");
72+
}
6573
}
6674
xfree(buf);
6775
if (consumed) {

‎spec/zstd-ruby-streaming-compress_spec.rb‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@
9191
end
9292
end
9393

94+
describe 'Zstd::CDict dictionary the caller does not keep' do
95+
let(:dictionary) do
96+
File.read("#{__dir__}/dictionary")
97+
end
98+
let(:user_json) do
99+
File.read("#{__dir__}/user_springmt.json")
100+
end
101+
it 'stays alive as long as the stream that references it' do
102+
# The CDict is never stored anywhere: the stream is its only reference.
103+
stream = Zstd::StreamingCompress.new(dict: Zstd::CDict.new(dictionary, 5))
104+
GC.start
105+
GC.compact
106+
1000.times { |i| "fill the slot the CDict would have freed #{i}" }
107+
GC.start
108+
109+
stream << user_json
110+
compressed = stream.finish
111+
112+
expect(Zstd.decompress(compressed, dict: dictionary)).to eq(user_json)
113+
end
114+
end
115+
94116
describe 'nil dictionary' do
95117
let(:user_json) do
96118
File.read("#{__dir__}/user_springmt.json")

‎spec/zstd-ruby-streaming-decompress_spec.rb‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,29 @@
138138
end
139139
end
140140

141+
describe 'Zstd::DDict dictionary the caller does not keep' do
142+
let(:dictionary) do
143+
File.read("#{__dir__}/dictionary")
144+
end
145+
let(:user_json) do
146+
File.read("#{__dir__}/user_springmt.json")
147+
end
148+
it 'stays alive as long as the stream that references it' do
149+
compressed_json = Zstd.compress(user_json, dict: dictionary)
150+
# The DDict is never stored anywhere: the stream is its only reference.
151+
stream = Zstd::StreamingDecompress.new(dict: Zstd::DDict.new(dictionary))
152+
GC.start
153+
GC.compact
154+
1000.times { |i| "fill the slot the DDict would have freed #{i}" }
155+
GC.start
156+
157+
result = +''
158+
result << stream.decompress(compressed_json[0, 5])
159+
result << stream.decompress(compressed_json[5..-1])
160+
expect(result).to eq(user_json)
161+
end
162+
end
163+
141164
describe 'nil dictionary streaming decompress + GC.compact' do
142165
let(:dictionary) do
143166
File.read("#{__dir__}/dictionary")

‎spec/zstd-ruby_spec.rb‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ def to_str
116116
expect { Zstd.decompress(Object.new) }.to raise_error(TypeError)
117117
end
118118

119+
it 'should raise (not hang) on a truncated frame' do
120+
full = Zstd.compress('a' * 2000)
121+
[
122+
"\x28\xB5\x2F\xFD".b, # bare zstd magic, no body
123+
full.byteslice(0, 5),
124+
full.byteslice(0, 6),
125+
full.byteslice(0, full.bytesize / 2),
126+
].each do |truncated|
127+
expect { Zstd.decompress(truncated) }.to raise_error(RuntimeError)
128+
end
129+
end
130+
119131
class DummyForDecompress
120132
def to_str
121133
Zstd.compress('abc')

0 commit comments

Comments
 (0)