From 92a87011993bf198a039bc8cbc9e89ce0642ec99 Mon Sep 17 00:00:00 2001 From: Samuel Giddins Date: Fri, 23 Oct 2026 22:05:41 -0700 Subject: [PATCH] Do not match the line separator inside a multibyte character in GzipReader#gets The separator was searched bytewise with memchr and memcmp, without checking that a match starts on a character boundary. In Shift_JIS the second byte of a character can be an ASCII byte such as "@", "a" or "|", so gets with such a separator cut the character in half. IO#gets only accepts a match on a boundary. ```ruby gz = Zlib.gzip("\x81\x40?\x81\x40@".b) r = Zlib::GzipReader.new(StringIO.new(gz), external_encoding: "Shift_JIS") r.gets("@") # expected: "\x{8140}?\x{8140}@" # actual: "\x{8140}" ``` Co-Authored-By: Claude Mythos 5.1 --- ext/zlib/zlib.c | 6 ++++++ test/zlib/test_zlib.rb | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/ext/zlib/zlib.c b/ext/zlib/zlib.c index 982933b..87087fa 100644 --- a/ext/zlib/zlib.c +++ b/ext/zlib/zlib.c @@ -4496,6 +4496,12 @@ gzreader_gets(int argc, VALUE *argv, VALUE obj) else { n += (rb_len_t)(res - p); p = res; + if (maxlen > 1 && + rb_enc_left_char_head(RSTRING_PTR(gz->z.buf), p, RSTRING_PTR(gz->z.buf) + ZSTREAM_BUF_FILLED(&gz->z), enc) != p) { + /* the match starts inside a multibyte character: keep searching */ + p++, n++; + continue; + } if (rslen == 1 || memcmp(p, rsptr, rslen) == 0) break; p++, n++; } diff --git a/test/zlib/test_zlib.rb b/test/zlib/test_zlib.rb index 1f9cb81..7e706f7 100644 --- a/test/zlib/test_zlib.rb +++ b/test/zlib/test_zlib.rb @@ -1367,6 +1367,20 @@ def test_reader_wrap } end + def test_gets_separator_inside_multibyte_char + pend "rb_io_extract_encoding_option ignores the encoding options on TruffleRuby" if RUBY_ENGINE == 'truffleruby' + # In Shift_JIS the second byte of a character can be an ASCII byte such + # as "@"; a separator must only match on a character boundary. + content = "\x81\x40?\x81\x40@".b + r = Zlib::GzipReader.new(StringIO.new(Zlib.gzip(content)), external_encoding: "Shift_JIS") + assert_equal(content.dup.force_encoding("Shift_JIS"), r.gets("@")) + assert_nil(r.gets("@")) + r = Zlib::GzipReader.new(StringIO.new(Zlib.gzip(content)), external_encoding: "Shift_JIS") + assert_equal("\x81\x40?".b.force_encoding("Shift_JIS"), r.gets("?")) + assert_equal("\x81\x40@".b.force_encoding("Shift_JIS"), r.gets("?")) + assert_nil(r.gets("?")) + end + def test_corrupted_header gz = Zlib::GzipWriter.new(StringIO.new(s = "".dup)) gz.orig_name = "X"