|
1 | 1 | # frozen_string_literal: true |
2 | 2 |
|
3 | 3 | require_relative "../test_helper" |
| 4 | +require "timeout" |
4 | 5 |
|
5 | 6 | module Prism |
6 | 7 | class ParseStreamTest < TestCase |
@@ -43,6 +44,18 @@ def test___END__ |
43 | 44 | assert_equal "5 + 6\n", io.read |
44 | 45 | end |
45 | 46 |
|
| 47 | + # __END__ as the final line with no trailing newline must still be detected |
| 48 | + # as the terminator. This exercises the EOF check for a chunk that does not |
| 49 | + # end in a newline, distinct from a line longer than the internal read |
| 50 | + # buffer (which also has no trailing newline but is not the end of input). |
| 51 | + def test___END___without_trailing_newline |
| 52 | + io = StringIO.new("1 + 2\n__END__") |
| 53 | + result = Prism.parse_stream(io) |
| 54 | + |
| 55 | + assert result.success? |
| 56 | + assert_equal 1, result.value.statements.body.length |
| 57 | + end |
| 58 | + |
46 | 59 | def test_false___END___in_string |
47 | 60 | io = StringIO.new(<<~RUBY) |
48 | 61 | 1 + 2 |
@@ -114,5 +127,70 @@ def test_nul_bytes |
114 | 127 | assert result.success? |
115 | 128 | assert_equal 3, result.value.statements.body.length |
116 | 129 | end |
| 130 | + |
| 131 | + # `IO#gets(limit)` will not split a multi-byte character, so it can return |
| 132 | + # more bytes than requested when the limit falls in the middle of one. When |
| 133 | + # that character straddles the internal read buffer boundary, the old code |
| 134 | + # wrote past the buffer and dropped the overflowing bytes, corrupting the |
| 135 | + # content (and overflowing the stack). Sweep offsets around the 4096-byte |
| 136 | + # boundary with both 3- and 4-byte characters so the straddle is hit |
| 137 | + # regardless of the exact internal read size. |
| 138 | + def test_multibyte_on_read_boundary |
| 139 | + ["あ", "\u{1F600}"].each do |char| |
| 140 | + (4080..4100).each do |prefix| |
| 141 | + body = ("a" * prefix) + char |
| 142 | + result = Prism.parse_stream(StringIO.new("\"#{body}\"")) |
| 143 | + |
| 144 | + assert result.success?, "parse failed at prefix=#{prefix} char=#{char.dump}" |
| 145 | + assert_equal body, result.value.statements.body[0].content, "content mismatch at prefix=#{prefix} char=#{char.dump}" |
| 146 | + end |
| 147 | + end |
| 148 | + end |
| 149 | + |
| 150 | + # A misbehaving stream whose `gets` ignores its limit and returns an |
| 151 | + # arbitrarily long string must not overflow the internal buffer. The old |
| 152 | + # code copied the full returned length into a fixed 4096-byte buffer. |
| 153 | + def test_gets_exceeding_limit |
| 154 | + stream = Object.new |
| 155 | + def stream.gets(limit = nil) |
| 156 | + return nil if defined?(@done) && @done |
| 157 | + @done = true |
| 158 | + ("x" * 100_000) + "\n" |
| 159 | + end |
| 160 | + def stream.eof?; defined?(@done) && @done; end |
| 161 | + |
| 162 | + result = Prism.parse_stream(stream) |
| 163 | + assert result.success? |
| 164 | + end |
| 165 | + |
| 166 | + # A misbehaving stream whose `gets` returns a non-String must not be read as |
| 167 | + # a byte buffer. The old code called RSTRING_PTR on whatever was returned. |
| 168 | + def test_gets_returning_non_string |
| 169 | + stream = Object.new |
| 170 | + def stream.gets(limit = nil) |
| 171 | + return nil if defined?(@done) && @done |
| 172 | + @done = true |
| 173 | + 1234 |
| 174 | + end |
| 175 | + def stream.eof?; defined?(@done) && @done; end |
| 176 | + |
| 177 | + assert_nothing_raised do |
| 178 | + Prism.parse_stream(stream) |
| 179 | + end |
| 180 | + end |
| 181 | + |
| 182 | + # A misbehaving stream whose `gets` returns an empty string instead of nil |
| 183 | + # while never reporting EOF must not loop forever. A well-behaved stream |
| 184 | + # returns nil at EOF, so an empty chunk is treated as the end of input. |
| 185 | + def test_gets_returning_empty_string |
| 186 | + stream = Object.new |
| 187 | + def stream.gets(limit = nil); ""; end |
| 188 | + def stream.eof?; false; end |
| 189 | + |
| 190 | + result = Timeout.timeout(10) { Prism.parse_stream(stream) } |
| 191 | + assert result.success? |
| 192 | + rescue Timeout::Error |
| 193 | + flunk "Prism.parse_stream looped forever on a stream returning empty strings" |
| 194 | + end |
117 | 195 | end |
118 | 196 | end |
0 commit comments