From 17b5426813997b3aca12515e580ba09f392333a6 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 15 Apr 2026 05:01:22 +0300 Subject: [PATCH] io: fix BufferedReader.read_line undocumented behavior on new line characters (fixes #18751) --- vlib/io/buffered_reader.v | 22 +++++++++++++--------- vlib/io/reader_test.v | 14 ++++++++++++++ vlib/io/string_reader/string_reader.v | 6 ++++-- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/vlib/io/buffered_reader.v b/vlib/io/buffered_reader.v index b72f467d4..bc35f155f 100644 --- a/vlib/io/buffered_reader.v +++ b/vlib/io/buffered_reader.v @@ -26,7 +26,7 @@ pub: @[params] pub struct BufferedReadLineConfig { pub: - delim u8 = `\n` // line delimiter + delim u8 = `\n` // line delimiter; `\n` also strips a preceding `\r` } // new_buffered_reader creates a new BufferedReader. @@ -120,8 +120,10 @@ pub fn (r BufferedReader) end_of_stream() bool { } // read_line attempts to read a line from the buffered reader. -// It will read until it finds the specified line delimiter -// such as (\n, the default or \0) or the end of stream. +// It reads until it finds the specified delimiter or the end of stream. +// The returned string does not include the delimiter. +// When the delimiter is `\n`, a preceding `\r` is treated as part of CRLF +// and is also omitted from the returned string. pub fn (mut r BufferedReader) read_line(config BufferedReadLineConfig) !string { if r.end_of_stream { return Eof{} @@ -146,13 +148,15 @@ pub fn (mut r BufferedReader) read_line(config BufferedReadLineConfig) !string { c := r.buf[i] if c == config.delim { // great, we hit something - // do some checking for whether we hit \r\n or just \n - if i != 0 && config.delim == `\n` && r.buf[i - 1] == `\r` { - x := i - 1 - line << r.buf[r.offset..x] - } else { - line << r.buf[r.offset..i] + mut end := i + if config.delim == `\n` { + if i > r.offset && r.buf[i - 1] == `\r` { + end-- + } else if i == r.offset && line.len > 0 && line[line.len - 1] == `\r` { + line.delete_last() + } } + line << r.buf[r.offset..end] r.offset = i + 1 return line.bytestr() } diff --git a/vlib/io/reader_test.v b/vlib/io/reader_test.v index 528bae796..5639bc13d 100644 --- a/vlib/io/reader_test.v +++ b/vlib/io/reader_test.v @@ -182,6 +182,20 @@ fn test_leftover() { assert r.end_of_stream() } +fn test_read_line_strips_crlf_across_buffer_fills() { + text := '12345\r\n67890\r\n' + mut s := StringReaderTest{ + text: text + } + mut r := new_buffered_reader(reader: s, cap: 2) + assert r.read_line()! == '12345' + assert r.read_line()! == '67890' + if _ := r.read_line() { + assert false + } + assert r.end_of_stream() +} + fn test_totalread_read() { text := 'Some testing text' mut s := StringReaderTest{ diff --git a/vlib/io/string_reader/string_reader.v b/vlib/io/string_reader/string_reader.v index edfce10b1..c8c399d2a 100644 --- a/vlib/io/string_reader/string_reader.v +++ b/vlib/io/string_reader/string_reader.v @@ -227,8 +227,10 @@ pub fn (mut r StringReader) read(mut buf []u8) !int { } // read_line attempts to read a line from the reader. -// It will read until it finds the specified line delimiter -// such as (\n, the default or \0) or the end of stream. +// It reads until it finds the specified delimiter or the end of stream. +// The returned string does not include the delimiter. +// When the delimiter is `\n`, a preceding `\r` is treated as part of CRLF +// and is also omitted from the returned string. @[direct_array_access] pub fn (mut r StringReader) read_line(config io.BufferedReadLineConfig) !string { if r.end_of_stream && r.needs_fill() { -- 2.39.5