Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions src/source.c
Original file line number Diff line number Diff line change
Expand Up @@ -365,28 +365,50 @@ bool
pm_source_stream_read(pm_source_t *source) {
pm_buffer_t *buffer = source->stream.buffer;

/*
* We should use rb_enc_mbmaxlen() of stream.external_encoding for
* the max character length but we don't want to do it here for
* performance. We can detect the max character length in all
* available encodings by the following command line in the Ruby
* source directory:
*
* grep -Erh "max (enc|byte) length" enc |
* grep -Eo '[0-9]+' |
* sort |
* tail -n1 # => 6
*/
#define MAX_ENC_LEN 6

#define LINE_SIZE 4096
char line[LINE_SIZE];

while (memset(line, '\n', LINE_SIZE), source->stream.fgets(line, LINE_SIZE, source->stream.stream) != NULL) {
size_t length = LINE_SIZE;
while (length > 0 && line[length - 1] == '\n') length--;
/*
* io.gets(size) may read size or more bytes when the last
* character is a multi-byte character. "+ MAX_ENC_LEN" is for the
* case.
*/
char line[LINE_SIZE + MAX_ENC_LEN];

/* This must not be '\0' to detect the `\0` written by fgets(). */
#define SENTINEL_CHAR 'x'

while (memset(line, SENTINEL_CHAR, LINE_SIZE + MAX_ENC_LEN), source->stream.fgets(line, LINE_SIZE, source->stream.stream) != NULL) {
Comment on lines +392 to +394

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep using \n for the sentinel character but I've changed to another character for easy to understand. If you don't like it, I'll revert it.

size_t length = LINE_SIZE + MAX_ENC_LEN;
while (length > 0 && line[length - 1] != '\0') length--;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep using == SENTINEL_CHAR not != '\0' here. I think that != '\0' is easier to understand but I'll revert this if you don't like it.


if (length == LINE_SIZE) {
/* Remove '\0' written by fgets() */
length -= 1;

/* Append the line to the buffer. */
pm_buffer_append_string(buffer, line, length);

if (line[length - 1] != '\n') {
/*
* If we read a line that is the maximum size and it doesn't end
* with a newline, then we'll just append it to the buffer and
* continue reading.
* If it doesn't end with a newline, then we'll continue
* reading.
*/
length--;
pm_buffer_append_string(buffer, line, length);
continue;
}

/* Append the line to the buffer. */
length--;
pm_buffer_append_string(buffer, line, length);

/*
* Check if the line matches the __END__ marker. If it does, then stop
* reading and return false. In most circumstances, this means we should
Expand Down Expand Up @@ -426,7 +448,9 @@ pm_source_stream_read(pm_source_t *source) {
}
}

#undef SENTINEL_CHAR
#undef LINE_SIZE
#undef MAX_ENC_LEN

source->stream.eof = true;
source->source = (const uint8_t *) pm_buffer_value(buffer);
Expand Down
14 changes: 14 additions & 0 deletions test/prism/api/parse_stream_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,19 @@ def test_nul_bytes
assert result.success?
assert_equal 3, result.value.statements.body.length
end

def test_long_line
# The important point of this test is that the character at 4095
# is a multi-byte character (U+3042 HIRAGANA LETTER A) in this
# case. gets(4095) may return 4095 or more bytes if the last
# character is a multi-byte character. For example,
# StringIO.new("\u3042").gets(1).bytesize is 3 not 1.
long_string = "\"" + ("a" * 4093) + "\u3042" + "\""
io = StringIO.new(long_string)
result = Prism.parse_stream(io)

assert result.success?
assert_equal long_string[1..-2], result.value.statements.body[0].content
end
end
end
Loading