diff --git a/lib/csv/parser.rb b/lib/csv/parser.rb index 25f10df..4996853 100644 --- a/lib/csv/parser.rb +++ b/lib/csv/parser.rb @@ -731,11 +731,11 @@ def resolve_row_separator(separator) end def detect_row_separator(sample, cr, lf) - lf_index = sample.index(lf) + lf_index = find_unquoted_string_index(sample, lf) if lf_index - cr_index = sample[0, lf_index].index(cr) + cr_index = find_unquoted_string_index(sample[0, lf_index], cr) else - cr_index = sample.index(cr) + cr_index = find_unquoted_string_index(sample, cr) end if cr_index and lf_index if cr_index + 1 == lf_index @@ -754,6 +754,21 @@ def detect_row_separator(sample, cr, lf) end end + # Index of the first +target+ that is outside a quoted field, so a CR or + # LF inside quotes is not mistaken for the row separator. + def find_unquoted_string_index(text, target) + return text.index(target) if @quote_character.nil? + in_quote = false + text.each_char.with_index do |char, i| + if char == @quote_character + in_quote = !in_quote + elsif char == target and not in_quote + return i + end + end + nil + end + def prepare_line @lineno = 0 @last_line = nil diff --git a/test/csv/parse/test_row_separator.rb b/test/csv/parse/test_row_separator.rb index 5fd8e75..53b9704 100644 --- a/test/csv/parse/test_row_separator.rb +++ b/test/csv/parse/test_row_separator.rb @@ -13,4 +13,33 @@ def test_multiple_characters CSV.parse("a\r\nb\r\n", row_sep: "\r\n")) end end + + # :auto detection must ignore CR/LF inside a quoted field. + def test_auto_quoted_cr + assert_equal([["a\rb", "x"]], + CSV.parse("\"a\rb\",x\n")) + end + + def test_auto_quoted_cr_lf + assert_equal([["a\r\nb", "x"]], + CSV.parse("\"a\r\nb\",x\n")) + end + + def test_auto_quoted_cr_only_field + assert_equal([["\r"]], + CSV.parse("\"\r\"\n")) + end + + # An unquoted CR is still a valid (old Mac) row separator. + def test_auto_unquoted_cr_still_detected + assert_equal([["a"], ["b"]], + CSV.parse("a\rb\r")) + end + + # CSV must be able to parse its own generated output. + def test_auto_round_trip_cr_fields + rows = [["a\rb", "x"], ["\r", "y"], ["c", "d\r\ne"]] + generated = CSV.generate {|csv| rows.each {|row| csv << row}} + assert_equal(rows, CSV.parse(generated)) + end end