From 477cd252de2e4b9f2c2e274b3c899715f20e4d95 Mon Sep 17 00:00:00 2001 From: Samuel Giddins Date: Fri, 23 Oct 2026 22:00:32 -0700 Subject: [PATCH] Fix GzipReader#gets(limit) splitting a multibyte character at the buffer end gzreader_charboundary finds how many bytes of the character at the limit are missing from the inflated buffer, but asked gzfile_fill for only n + missing bytes. When the buffer already held more than n bytes nothing new was read, and the character was cut. The inflated buffer ends inside a character whenever a 2048-byte read of compressed data does. ```ruby content = (("\u3042\u3044\u3046" * 700) + "\n") * 3 r = Zlib::GzipReader.new(StringIO.new(Zlib.gzip(content, level: 0))) lines = [] while (line = r.gets(1)) do lines << line end lines.reject(&:valid_encoding?).size # expected: 0 # actual: 2 ``` Request the buffer length plus the missing bytes instead. Fixes https://github.com/ruby/zlib/issues/92 Co-Authored-By: Claude Mythos 5.1 --- ext/zlib/zlib.c | 4 +++- test/zlib/test_zlib.rb | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ext/zlib/zlib.c b/ext/zlib/zlib.c index 982933b..2771ff6 100644 --- a/ext/zlib/zlib.c +++ b/ext/zlib/zlib.c @@ -4376,7 +4376,9 @@ gzreader_charboundary(struct gzfile *gz, rb_len_t n) if (l < n) { int n_bytes = rb_enc_precise_mbclen(p, e, gz->enc); if (MBCLEN_NEEDMORE_P(n_bytes)) { - if ((l = gzfile_fill(gz, n + MBCLEN_NEEDMORE_LEN(n_bytes), Qnil)) > 0) { + /* the character needs MBCLEN_NEEDMORE_LEN bytes beyond the END of the + * buffer, which may lie past n */ + if ((l = gzfile_fill(gz, ZSTREAM_BUF_FILLED(&gz->z) + MBCLEN_NEEDMORE_LEN(n_bytes), Qnil)) > 0) { return l; } } diff --git a/test/zlib/test_zlib.rb b/test/zlib/test_zlib.rb index 1f9cb81..b932034 100644 --- a/test/zlib/test_zlib.rb +++ b/test/zlib/test_zlib.rb @@ -1367,6 +1367,22 @@ def test_reader_wrap } end + def test_gets_limit_multibyte_boundary + # The inflated buffer ends in the middle of a character whenever a + # 2048-byte read of compressed data does; gets(limit) must still return + # whole characters (same as IO#gets(limit)). + content = (("\u3042\u3044\u3046" * 700) + "\n") * 3 + [1, 4, 7, 10].each do |limit| + r = Zlib::GzipReader.new(StringIO.new(Zlib.gzip(content, level: 0))) + lines = [] + while (line = r.gets(limit)) + lines << line + end + assert_equal(content, lines.join) + assert_empty(lines.reject(&:valid_encoding?), "limit #{limit}") + end + end + def test_corrupted_header gz = Zlib::GzipWriter.new(StringIO.new(s = "".dup)) gz.orig_name = "X"