Skip to content

Commit

Permalink
Fix eof? to return true when there is no more data to read, even if t…
Browse files Browse the repository at this point in the history
…he stream is not yet finished

See code comments for details

Fixes ruby#56
  • Loading branch information
segiddins committed Dec 22, 2023
1 parent 2561e12 commit 6973398
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 0 deletions.
11 changes: 11 additions & 0 deletions ext/zlib/zlib.c
Original file line number Diff line number Diff line change
Expand Up @@ -3500,6 3500,14 @@ static VALUE
rb_gzfile_eof_p(VALUE obj)
{
struct gzfile *gz = get_gzfile(obj);

// Similar to Socket/Pipe, we need to check if there is any actual data left,
// as it is possible that the stream is not finished, but there is no data that
// would be returned upon reading, so we want to report eof? in that case
while (!ZSTREAM_IS_FINISHED(&gz->z) && ZSTREAM_BUF_FILLED(&gz->z) == 0) {
gzfile_read_more(gz, Qnil);
}

return GZFILE_IS_FINISHED(gz) ? Qtrue : Qfalse;
}

Expand Down Expand Up @@ -4830,6 4838,9 @@ Init_zlib(void)
rb_define_attr(cGzError, "input", 1, 0);
rb_define_method(cGzError, "inspect", gzfile_error_inspect, 0);

/* The block size in bytes that the gzip file reads from its underlying io */
rb_define_const(cGzipFile, "READ_SIZE", INT2FIX(GZFILE_READ_SIZE));

cNoFooter = rb_define_class_under(cGzipFile, "NoFooter", cGzError);
cCRCError = rb_define_class_under(cGzipFile, "CRCError", cGzError);
cLengthError = rb_define_class_under(cGzipFile,"LengthError",cGzError);
Expand Down
63 changes: 63 additions & 0 deletions test/zlib/test_zlib.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 1205,69 @@ def test_double_close
}
end

# Test boundary cases around compressed data where the "final" deflate block is empty,
# and close to a Zlib::GzipFile::READ_SIZE boundary
[(Zlib::GzipFile::READ_SIZE - 30)..(Zlib::GzipFile::READ_SIZE - 10), (Zlib::GzipFile::READ_SIZE * 2 - 30)..(Zlib::GzipFile::READ_SIZE * 2 - 10)].flat_map(&:to_a).each do |size|
contents = "a\n" * (size / 2)
contents << "\n" if size.odd?
contents.freeze

s = StringIO.new
gz = Zlib::GzipWriter.new(s, Zlib::NO_COMPRESSION)
gz.write contents
gz.flush
gz.close
s = s.string.freeze

sub_test_case("String of length #{size}") do
[[:readpartial, 500], [:readpartial, 1024], [:readpartial, 2028], [:gets], [:getc], [:getbyte], [:read], [:read, 500], [:read, 1024], [:read, 2048], [:readline], [:readlines], [:readbyte], [:readchar]].each do |method, *args|
test("#{method}(#{args.join(', ')})") do
read = "".b
Zlib::GzipReader.wrap(StringIO.new(s)) do |gzio|
until gzio.eof?
part = gzio.send(method, *args)
refute_nil part
case part
when Array then part.each { |e| read << e }
else read << part
end
end
assert_predicate gzio, :eof?

case method
when :readbyte, :getbyte
b = read.bytes.last
gzio.ungetbyte b
refute_predicate gzio, :eof?
assert_equal b, gzio.send(method, *args)
assert_predicate gzio, :eof?
when :readchar, :getc
c = read[-1]
gzio.ungetc c
refute_predicate gzio, :eof?
assert_equal c, gzio.send(method, *args)
assert_predicate gzio, :eof?
end

2.times do
case method
when :readpartial, :readbyte, :readchar, :readline
assert_raise(EOFError) { gzio.send(method, *args) }
when :readlines
assert_equal [], gzio.send(method, *args)
when ->(m) { m == :read && args.empty? }
assert_equal "", gzio.send(method, *args)
else
assert_nil gzio.send(method, *args)
end
assert_predicate gzio, :eof?
end
end
assert_equal contents, read
end
end
end
end
end

class TestZlibGzipWriter < Test::Unit::TestCase
Expand Down

0 comments on commit 6973398

Please sign in to comment.