From 5a4aac9250655bf0d8739d25451dfa558116e289 Mon Sep 17 00:00:00 2001 From: Bala Kumar Date: Mon, 5 Oct 2026 21:47:22 +0530 Subject: [PATCH] Keep the input in the result of Zstd.write_skippable_frame write_skippable_frame(input, data) allocated room for the input plus the skippable frame, but returned only the frame: ZSTD_writeSkippableFrame writes the frame from the start of the buffer and the result was then truncated to the frame's size. The input was discarded, so the README example's compressed_data_with_skippable_frame no longer contained the compressed data, and Zstd.decompress on it raised "not a zstd frame". Write the frame and then the input after it. A leading frame is what read_skippable_frame expects, and Zstd.decompress skips skippable frames to reach the data that follows. --- ext/zstdruby/skippable_frame.c | 17 ++++++++++------ spec/zstd-skippable_frame_spec.rb | 32 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/ext/zstdruby/skippable_frame.c b/ext/zstdruby/skippable_frame.c index 0920b14..9eaf4f7 100644 --- a/ext/zstdruby/skippable_frame.c +++ b/ext/zstdruby/skippable_frame.c @@ -21,15 +21,20 @@ static VALUE rb_write_skippable_frame(int argc, VALUE *argv, VALUE self) char* skip_data = RSTRING_PTR(skip_value); size_t skip_size = RSTRING_LEN(skip_value); - size_t dst_size = input_size + ZSTD_SKIPPABLEHEADERSIZE + skip_size; - VALUE output = rb_str_new(NULL, dst_size); + /* The skippable frame goes first, followed by the input: read_skippable_frame + reads a frame at the start, and decompress skips it to reach the input. */ + size_t frame_capacity = ZSTD_SKIPPABLEHEADERSIZE + skip_size; + VALUE output = rb_str_new(NULL, frame_capacity + input_size); char* output_data = RSTRING_PTR(output); - size_t output_size = ZSTD_writeSkippableFrame((void*)output_data, dst_size, (const void*)skip_data, skip_size, magic_variant); - if (ZSTD_isError(output_size)) { - rb_raise(rb_eRuntimeError, "%s: %s", "write skippable frame failed", ZSTD_getErrorName(output_size)); + size_t frame_size = ZSTD_writeSkippableFrame((void*)output_data, frame_capacity, (const void*)skip_data, skip_size, magic_variant); + if (ZSTD_isError(frame_size)) { + rb_raise(rb_eRuntimeError, "%s: %s", "write skippable frame failed", ZSTD_getErrorName(frame_size)); } + memcpy(output_data + frame_size, RSTRING_PTR(input_value), input_size); - rb_str_resize(output, output_size); + rb_str_resize(output, frame_size + input_size); + RB_GC_GUARD(input_value); + RB_GC_GUARD(skip_value); return output; } diff --git a/spec/zstd-skippable_frame_spec.rb b/spec/zstd-skippable_frame_spec.rb index eddd7f8..f407ce0 100644 --- a/spec/zstd-skippable_frame_spec.rb +++ b/spec/zstd-skippable_frame_spec.rb @@ -57,4 +57,36 @@ end end end + + describe 'write_skippable_frame' do + let(:payload) { SecureRandom.hex(150) } + let(:compressed_data) { Zstd.compress(payload) } + + it 'returns the skippable frame followed by the input' do + result = Zstd.write_skippable_frame(compressed_data, "sample data") + + expect(result.bytesize).to eq(8 + "sample data".bytesize + compressed_data.bytesize) + expect(result).to end_with(compressed_data) + end + + it 'keeps the compressed data decodable by Zstd.decompress' do + result = Zstd.write_skippable_frame(compressed_data, "sample data") + + expect(Zstd.decompress(result)).to eq payload + end + + it 'keeps the compressed data with a magic_variant' do + result = Zstd.write_skippable_frame(compressed_data, "sample data", magic_variant: 15) + + expect(Zstd.read_skippable_frame(result)).to eq "sample data" + expect(Zstd.decompress(result)).to eq payload + end + + it 'accepts an empty input' do + result = Zstd.write_skippable_frame("", "sample data") + + expect(result.bytesize).to eq(8 + "sample data".bytesize) + expect(Zstd.read_skippable_frame(result)).to eq "sample data" + end + end end