csv 3.2.2 → 3.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/csv/parser.rb CHANGED
@@ -2,15 +2,10 @@
2
2
 
3
3
  require "strscan"
4
4
 
5
- require_relative "delete_suffix"
6
5
  require_relative "input_record_separator"
7
- require_relative "match_p"
8
6
  require_relative "row"
9
7
  require_relative "table"
10
8
 
11
- using CSV::DeleteSuffix if CSV.const_defined?(:DeleteSuffix)
12
- using CSV::MatchP if CSV.const_defined?(:MatchP)
13
-
14
9
  class CSV
15
10
  # Note: Don't use this class directly. This is an internal class.
16
11
  class Parser
@@ -23,10 +18,27 @@ class CSV
23
18
  # into your Encoding.
24
19
  #
25
20
 
21
+ class << self
22
+ ARGF_OBJECT_ID = ARGF.object_id
23
+ # Convenient method to check whether the give input reached EOF
24
+ # or not.
25
+ def eof?(input)
26
+ # We can't use input != ARGF in Ractor. Because ARGF isn't a
27
+ # shareable object.
28
+ input.object_id != ARGF_OBJECT_ID and
29
+ input.respond_to?(:eof) and
30
+ input.eof?
31
+ end
32
+ end
33
+
26
34
  # Raised when encoding is invalid.
27
35
  class InvalidEncoding < StandardError
28
36
  end
29
37
 
38
+ # Raised when unexpected case is happen.
39
+ class UnexpectedError < StandardError
40
+ end
41
+
30
42
  #
31
43
  # CSV::Scanner receives a CSV output, scans it and return the content.
32
44
  # It also controls the life cycle of the object with its methods +keep_start+,
@@ -35,7 +47,7 @@ class CSV
35
47
  # Uses StringScanner (the official strscan gem). Strscan provides lexical
36
48
  # scanning operations on a String. We inherit its object and take advantage
37
49
  # on the methods. For more information, please visit:
38
- # https://ruby-doc.org/stdlib-2.6.1/libdoc/strscan/rdoc/StringScanner.html
50
+ # https://docs.ruby-lang.org/en/master/StringScanner.html
39
51
  #
40
52
  class Scanner < StringScanner
41
53
  alias_method :scan_all, :scan
@@ -78,10 +90,10 @@ class CSV
78
90
  # +keep_end+, +keep_back+, +keep_drop+.
79
91
  #
80
92
  # CSV::InputsScanner.scan() tries to match with pattern at the current position.
81
- # If there's a match, the scanner advances the scan pointer and returns the matched string.
93
+ # If there's a match, the scanner advances the "scan pointer" and returns the matched string.
82
94
  # Otherwise, the scanner returns nil.
83
95
  #
84
- # CSV::InputsScanner.rest() returns the rest of the string (i.e. everything after the scan pointer).
96
+ # CSV::InputsScanner.rest() returns the "rest" of the string (i.e. everything after the scan pointer).
85
97
  # If there is no more data (eos? = true), it returns "".
86
98
  #
87
99
  class InputsScanner
@@ -96,11 +108,13 @@ class CSV
96
108
  end
97
109
 
98
110
  def each_line(row_separator)
111
+ return enum_for(__method__, row_separator) unless block_given?
99
112
  buffer = nil
100
113
  input = @scanner.rest
101
114
  position = @scanner.pos
102
115
  offset = 0
103
116
  n_row_separator_chars = row_separator.size
117
+ # trace(__method__, :start, input)
104
118
  while true
105
119
  input.each_line(row_separator) do |line|
106
120
  @scanner.pos += line.bytesize
@@ -140,25 +154,29 @@ class CSV
140
154
  end
141
155
 
142
156
  def scan(pattern)
157
+ # trace(__method__, pattern, :start)
143
158
  value = @scanner.scan(pattern)
159
+ # trace(__method__, pattern, :done, :last, value) if @last_scanner
144
160
  return value if @last_scanner
145
161
 
146
- if value
147
- read_chunk if @scanner.eos?
148
- return value
149
- else
150
- nil
151
- end
162
+ read_chunk if value and @scanner.eos?
163
+ # trace(__method__, pattern, :done, value)
164
+ value
152
165
  end
153
166
 
154
167
  def scan_all(pattern)
168
+ # trace(__method__, pattern, :start)
155
169
  value = @scanner.scan(pattern)
170
+ # trace(__method__, pattern, :done, :last, value) if @last_scanner
156
171
  return value if @last_scanner
157
172
 
173
+ # trace(__method__, pattern, :done, :nil) if value.nil?
158
174
  return nil if value.nil?
159
175
  while @scanner.eos? and read_chunk and (sub_value = @scanner.scan(pattern))
176
+ # trace(__method__, pattern, :sub, sub_value)
160
177
  value << sub_value
161
178
  end
179
+ # trace(__method__, pattern, :done, value)
162
180
  value
163
181
  end
164
182
 
@@ -167,68 +185,136 @@ class CSV
167
185
  end
168
186
 
169
187
  def keep_start
170
- @keeps.push([@scanner.pos, nil])
188
+ # trace(__method__, :start)
189
+ adjust_last_keep
190
+ @keeps.push([@scanner, @scanner.pos, nil])
191
+ # trace(__method__, :done)
171
192
  end
172
193
 
173
194
  def keep_end
174
- start, buffer = @keeps.pop
175
- keep = @scanner.string.byteslice(start, @scanner.pos - start)
195
+ # trace(__method__, :start)
196
+ scanner, start, buffer = @keeps.pop
197
+ if scanner == @scanner
198
+ keep = @scanner.string.byteslice(start, @scanner.pos - start)
199
+ else
200
+ keep = @scanner.string.byteslice(0, @scanner.pos)
201
+ end
176
202
  if buffer
177
203
  buffer << keep
178
204
  keep = buffer
179
205
  end
206
+ # trace(__method__, :done, keep)
180
207
  keep
181
208
  end
182
209
 
183
210
  def keep_back
184
- start, buffer = @keeps.pop
211
+ # trace(__method__, :start)
212
+ scanner, start, buffer = @keeps.pop
185
213
  if buffer
214
+ # trace(__method__, :rescan, start, buffer)
186
215
  string = @scanner.string
187
- keep = string.byteslice(start, string.bytesize - start)
216
+ if scanner == @scanner
217
+ keep = string.byteslice(start,
218
+ string.bytesize - @scanner.pos - start)
219
+ else
220
+ keep = string
221
+ end
188
222
  if keep and not keep.empty?
189
223
  @inputs.unshift(StringIO.new(keep))
190
224
  @last_scanner = false
191
225
  end
192
226
  @scanner = StringScanner.new(buffer)
193
227
  else
228
+ if @scanner != scanner
229
+ message = "scanners are different but no buffer: "
230
+ message += "#{@scanner.inspect}(#{@scanner.object_id}): "
231
+ message += "#{scanner.inspect}(#{scanner.object_id})"
232
+ raise UnexpectedError, message
233
+ end
234
+ # trace(__method__, :repos, start, buffer)
194
235
  @scanner.pos = start
236
+ last_scanner, last_start, last_buffer = @keeps.last
237
+ # Drop the last buffer when the last buffer is the same data
238
+ # in the last keep. If we keep it, we have duplicated data
239
+ # by the next keep_back.
240
+ if last_scanner == @scanner and
241
+ last_buffer and
242
+ last_buffer == last_scanner.string.byteslice(last_start, start)
243
+ @keeps.last[2] = nil
244
+ end
195
245
  end
196
246
  read_chunk if @scanner.eos?
197
247
  end
198
248
 
199
249
  def keep_drop
200
- @keeps.pop
250
+ _, _, buffer = @keeps.pop
251
+ # trace(__method__, :done, :empty) unless buffer
252
+ return unless buffer
253
+
254
+ last_keep = @keeps.last
255
+ # trace(__method__, :done, :no_last_keep) unless last_keep
256
+ return unless last_keep
257
+
258
+ if last_keep[2]
259
+ last_keep[2] << buffer
260
+ else
261
+ last_keep[2] = buffer
262
+ end
263
+ # trace(__method__, :done)
201
264
  end
202
265
 
203
266
  def rest
204
267
  @scanner.rest
205
268
  end
206
269
 
270
+ def check(pattern)
271
+ @scanner.check(pattern)
272
+ end
273
+
207
274
  private
208
- def read_chunk
209
- return false if @last_scanner
275
+ def trace(*args)
276
+ pp([*args, @scanner, @scanner&.string, @scanner&.pos, @keeps])
277
+ end
210
278
 
211
- unless @keeps.empty?
212
- keep = @keeps.last
213
- keep_start = keep[0]
214
- string = @scanner.string
215
- keep_data = string.byteslice(keep_start, @scanner.pos - keep_start)
216
- if keep_data
217
- keep_buffer = keep[1]
218
- if keep_buffer
219
- keep_buffer << keep_data
220
- else
221
- keep[1] = keep_data.dup
222
- end
279
+ def adjust_last_keep
280
+ # trace(__method__, :start)
281
+
282
+ keep = @keeps.last
283
+ # trace(__method__, :done, :empty) if keep.nil?
284
+ return if keep.nil?
285
+
286
+ scanner, start, buffer = keep
287
+ string = @scanner.string
288
+ if @scanner != scanner
289
+ start = 0
290
+ end
291
+ if start == 0 and @scanner.eos?
292
+ keep_data = string
293
+ else
294
+ keep_data = string.byteslice(start, @scanner.pos - start)
295
+ end
296
+ if keep_data
297
+ if buffer
298
+ buffer << keep_data
299
+ else
300
+ keep[2] = keep_data.dup
223
301
  end
224
- keep[0] = 0
225
302
  end
226
303
 
304
+ # trace(__method__, :done)
305
+ end
306
+
307
+ def read_chunk
308
+ return false if @last_scanner
309
+
310
+ adjust_last_keep
311
+
227
312
  input = @inputs.first
228
313
  case input
229
314
  when StringIO
230
315
  string = input.read
231
316
  raise InvalidEncoding unless string.valid_encoding?
317
+ # trace(__method__, :stringio, string)
232
318
  @scanner = StringScanner.new(string)
233
319
  @inputs.shift
234
320
  @last_scanner = @inputs.empty?
@@ -237,13 +323,15 @@ class CSV
237
323
  chunk = input.gets(@row_separator, @chunk_size)
238
324
  if chunk
239
325
  raise InvalidEncoding unless chunk.valid_encoding?
326
+ # trace(__method__, :chunk, chunk)
240
327
  @scanner = StringScanner.new(chunk)
241
- if input.respond_to?(:eof?) and input.eof?
328
+ if Parser.eof?(input)
242
329
  @inputs.shift
243
330
  @last_scanner = @inputs.empty?
244
331
  end
245
332
  true
246
333
  else
334
+ # trace(__method__, :no_chunk)
247
335
  @scanner = StringScanner.new("".encode(@encoding))
248
336
  @inputs.shift
249
337
  @last_scanner = @inputs.empty?
@@ -278,7 +366,11 @@ class CSV
278
366
  end
279
367
 
280
368
  def field_size_limit
281
- @field_size_limit
369
+ @max_field_size&.succ
370
+ end
371
+
372
+ def max_field_size
373
+ @max_field_size
282
374
  end
283
375
 
284
376
  def skip_lines
@@ -330,21 +422,24 @@ class CSV
330
422
 
331
423
  begin
332
424
  @scanner ||= build_scanner
333
- if quote_character.nil?
334
- parse_no_quote(&block)
335
- elsif @need_robust_parsing
336
- parse_quotable_robust(&block)
425
+ __send__(@parse_method, &block)
426
+ rescue InvalidEncoding
427
+ if @scanner
428
+ ignore_broken_line
429
+ lineno = @lineno
337
430
  else
338
- parse_quotable_loose(&block)
431
+ lineno = @lineno + 1
339
432
  end
340
- rescue InvalidEncoding
433
+ raise InvalidEncodingError.new(@encoding, lineno)
434
+ rescue UnexpectedError => error
341
435
  if @scanner
342
436
  ignore_broken_line
343
437
  lineno = @lineno
344
438
  else
345
439
  lineno = @lineno + 1
346
440
  end
347
- message = "Invalid byte sequence in #{@encoding}"
441
+ message = "This should not be happen: #{error.message}: "
442
+ message += "Please report this to https://github.com/ruby/csv/issues"
348
443
  raise MalformedCSVError.new(message, lineno)
349
444
  end
350
445
  end
@@ -371,7 +466,6 @@ class CSV
371
466
  end
372
467
 
373
468
  def prepare_variable
374
- @need_robust_parsing = false
375
469
  @encoding = @options[:encoding]
376
470
  liberal_parsing = @options[:liberal_parsing]
377
471
  if liberal_parsing
@@ -384,13 +478,12 @@ class CSV
384
478
  @double_quote_outside_quote = false
385
479
  @backslash_quote = false
386
480
  end
387
- @need_robust_parsing = true
388
481
  else
389
482
  @liberal_parsing = false
390
483
  @backslash_quote = false
391
484
  end
392
485
  @unconverted_fields = @options[:unconverted_fields]
393
- @field_size_limit = @options[:field_size_limit]
486
+ @max_field_size = @options[:max_field_size]
394
487
  @skip_blanks = @options[:skip_blanks]
395
488
  @fields_converter = @options[:fields_converter]
396
489
  @header_fields_converter = @options[:header_fields_converter]
@@ -407,7 +500,6 @@ class CSV
407
500
  message = ":quote_char has to be nil or a single character String"
408
501
  raise ArgumentError, message
409
502
  end
410
- @double_quote_character = @quote_character * 2
411
503
  @escaped_quote_character = Regexp.escape(@quote_character)
412
504
  @escaped_quote = Regexp.new(@escaped_quote_character)
413
505
  end
@@ -467,7 +559,6 @@ class CSV
467
559
  @rstrip_value = Regexp.new(@escaped_strip +
468
560
  "+\\z".encode(@encoding))
469
561
  end
470
- @need_robust_parsing = true
471
562
  elsif @strip
472
563
  strip_values = " \t\f\v"
473
564
  @escaped_strip = strip_values.encode(@encoding)
@@ -475,7 +566,6 @@ class CSV
475
566
  @strip_value = Regexp.new("[#{strip_values}]+".encode(@encoding))
476
567
  @rstrip_value = Regexp.new("[#{strip_values}]+\\z".encode(@encoding))
477
568
  end
478
- @need_robust_parsing = true
479
569
  end
480
570
  end
481
571
 
@@ -680,9 +770,10 @@ class CSV
680
770
  case headers
681
771
  when Array
682
772
  @raw_headers = headers
773
+ quoted_fields = FieldsConverter::NO_QUOTED_FIELDS
683
774
  @use_headers = true
684
775
  when String
685
- @raw_headers = parse_headers(headers)
776
+ @raw_headers, quoted_fields = parse_headers(headers)
686
777
  @use_headers = true
687
778
  when nil, false
688
779
  @raw_headers = nil
@@ -692,27 +783,41 @@ class CSV
692
783
  @use_headers = true
693
784
  end
694
785
  if @raw_headers
695
- @headers = adjust_headers(@raw_headers)
786
+ @headers = adjust_headers(@raw_headers, quoted_fields)
696
787
  else
697
788
  @headers = nil
698
789
  end
699
790
  end
700
791
 
701
792
  def parse_headers(row)
702
- CSV.parse_line(row,
703
- col_sep: @column_separator,
704
- row_sep: @row_separator,
705
- quote_char: @quote_character)
793
+ quoted_fields = []
794
+ converter = lambda do |field, info|
795
+ quoted_fields << info.quoted?
796
+ field
797
+ end
798
+ headers = CSV.parse_line(row,
799
+ col_sep: @column_separator,
800
+ row_sep: @row_separator,
801
+ quote_char: @quote_character,
802
+ converters: [converter])
803
+ [headers, quoted_fields]
706
804
  end
707
805
 
708
- def adjust_headers(headers)
709
- adjusted_headers = @header_fields_converter.convert(headers, nil, @lineno)
806
+ def adjust_headers(headers, quoted_fields)
807
+ adjusted_headers = @header_fields_converter.convert(headers, nil, @lineno, quoted_fields)
710
808
  adjusted_headers.each {|h| h.freeze if h.is_a? String}
711
809
  adjusted_headers
712
810
  end
713
811
 
714
812
  def prepare_parser
715
813
  @may_quoted = may_quoted?
814
+ if @quote_character.nil?
815
+ @parse_method = :parse_no_quote
816
+ elsif @liberal_parsing or @strip
817
+ @parse_method = :parse_quotable_robust
818
+ else
819
+ @parse_method = :parse_quotable_loose
820
+ end
716
821
  end
717
822
 
718
823
  def may_quoted?
@@ -729,28 +834,28 @@ class CSV
729
834
  sample[0, 128].index(@quote_character)
730
835
  end
731
836
 
732
- SCANNER_TEST = (ENV["CSV_PARSER_SCANNER_TEST"] == "yes")
733
- if SCANNER_TEST
734
- class UnoptimizedStringIO
735
- def initialize(string)
736
- @io = StringIO.new(string, "rb:#{string.encoding}")
737
- end
837
+ class UnoptimizedStringIO # :nodoc:
838
+ def initialize(string)
839
+ @io = StringIO.new(string, "rb:#{string.encoding}")
840
+ end
738
841
 
739
- def gets(*args)
740
- @io.gets(*args)
741
- end
842
+ def gets(*args)
843
+ @io.gets(*args)
844
+ end
742
845
 
743
- def each_line(*args, &block)
744
- @io.each_line(*args, &block)
745
- end
846
+ def each_line(*args, &block)
847
+ @io.each_line(*args, &block)
848
+ end
746
849
 
747
- def eof?
748
- @io.eof?
749
- end
850
+ def eof?
851
+ @io.eof?
750
852
  end
853
+ end
751
854
 
752
- SCANNER_TEST_CHUNK_SIZE =
753
- Integer((ENV["CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"] || "1"), 10)
855
+ SCANNER_TEST = (ENV["CSV_PARSER_SCANNER_TEST"] == "yes")
856
+ if SCANNER_TEST
857
+ SCANNER_TEST_CHUNK_SIZE_NAME = "CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"
858
+ SCANNER_TEST_CHUNK_SIZE_VALUE = ENV[SCANNER_TEST_CHUNK_SIZE_NAME]
754
859
  def build_scanner
755
860
  inputs = @samples.collect do |sample|
756
861
  UnoptimizedStringIO.new(sample)
@@ -760,20 +865,24 @@ class CSV
760
865
  else
761
866
  inputs << @input
762
867
  end
868
+ begin
869
+ chunk_size_value = ENV[SCANNER_TEST_CHUNK_SIZE_NAME]
870
+ rescue # Ractor::IsolationError
871
+ # Ractor on Ruby 3.0 can't read ENV value.
872
+ chunk_size_value = SCANNER_TEST_CHUNK_SIZE_VALUE
873
+ end
874
+ chunk_size = Integer((chunk_size_value || "1"), 10)
763
875
  InputsScanner.new(inputs,
764
876
  @encoding,
765
877
  @row_separator,
766
- chunk_size: SCANNER_TEST_CHUNK_SIZE)
878
+ chunk_size: chunk_size)
767
879
  end
768
880
  else
769
881
  def build_scanner
770
882
  string = nil
771
883
  if @samples.empty? and @input.is_a?(StringIO)
772
884
  string = @input.read
773
- elsif @samples.size == 1 and
774
- @input != ARGF and
775
- @input.respond_to?(:eof?) and
776
- @input.eof?
885
+ elsif @samples.size == 1 and Parser.eof?(@input)
777
886
  string = @samples[0]
778
887
  end
779
888
  if string
@@ -782,8 +891,7 @@ class CSV
782
891
  !line.valid_encoding?
783
892
  end
784
893
  if index
785
- message = "Invalid byte sequence in #{@encoding}"
786
- raise MalformedCSVError.new(message, @lineno + index + 1)
894
+ raise InvalidEncodingError.new(@encoding, @lineno + index + 1)
787
895
  end
788
896
  end
789
897
  Scanner.new(string)
@@ -826,6 +934,14 @@ class CSV
826
934
  end
827
935
  end
828
936
 
937
+ def validate_field_size(field)
938
+ return unless @max_field_size
939
+ return if field.size <= @max_field_size
940
+ ignore_broken_line
941
+ message = "Field size exceeded: #{field.size} > #{@max_field_size}"
942
+ raise MalformedCSVError.new(message, @lineno)
943
+ end
944
+
829
945
  def parse_no_quote(&block)
830
946
  @scanner.each_line(@row_separator) do |line|
831
947
  next if @skip_lines and skip_line?(line)
@@ -838,6 +954,11 @@ class CSV
838
954
  else
839
955
  line = strip_value(line)
840
956
  row = line.split(@split_column_separator, -1)
957
+ if @max_field_size
958
+ row.each do |column|
959
+ validate_field_size(column)
960
+ end
961
+ end
841
962
  n_columns = row.size
842
963
  i = 0
843
964
  while i < n_columns
@@ -868,31 +989,37 @@ class CSV
868
989
  next
869
990
  end
870
991
  row = []
992
+ quoted_fields = FieldsConverter::NO_QUOTED_FIELDS
871
993
  elsif line.include?(@cr) or line.include?(@lf)
872
994
  @scanner.keep_back
873
- @need_robust_parsing = true
995
+ @parse_method = :parse_quotable_robust
874
996
  return parse_quotable_robust(&block)
875
997
  else
876
998
  row = line.split(@split_column_separator, -1)
999
+ quoted_fields = []
877
1000
  n_columns = row.size
878
1001
  i = 0
879
1002
  while i < n_columns
880
1003
  column = row[i]
881
1004
  if column.empty?
1005
+ quoted_fields << false
882
1006
  row[i] = nil
883
1007
  else
884
1008
  n_quotes = column.count(@quote_character)
885
1009
  if n_quotes.zero?
1010
+ quoted_fields << false
886
1011
  # no quote
887
1012
  elsif n_quotes == 2 and
888
1013
  column.start_with?(@quote_character) and
889
1014
  column.end_with?(@quote_character)
1015
+ quoted_fields << true
890
1016
  row[i] = column[1..-2]
891
1017
  else
892
1018
  @scanner.keep_back
893
- @need_robust_parsing = true
1019
+ @parse_method = :parse_quotable_robust
894
1020
  return parse_quotable_robust(&block)
895
1021
  end
1022
+ validate_field_size(row[i])
896
1023
  end
897
1024
  i += 1
898
1025
  end
@@ -900,13 +1027,14 @@ class CSV
900
1027
  @scanner.keep_drop
901
1028
  @scanner.keep_start
902
1029
  @last_line = original_line
903
- emit_row(row, &block)
1030
+ emit_row(row, quoted_fields, &block)
904
1031
  end
905
1032
  @scanner.keep_drop
906
1033
  end
907
1034
 
908
1035
  def parse_quotable_robust(&block)
909
1036
  row = []
1037
+ quoted_fields = []
910
1038
  skip_needless_lines
911
1039
  start_row
912
1040
  while true
@@ -916,32 +1044,39 @@ class CSV
916
1044
  value = parse_column_value
917
1045
  if value
918
1046
  @scanner.scan_all(@strip_value) if @strip_value
919
- if @field_size_limit and value.size >= @field_size_limit
920
- ignore_broken_line
921
- raise MalformedCSVError.new("Field size exceeded", @lineno)
922
- end
1047
+ validate_field_size(value)
923
1048
  end
924
1049
  if parse_column_end
925
1050
  row << value
1051
+ quoted_fields << @quoted_column_value
926
1052
  elsif parse_row_end
927
1053
  if row.empty? and value.nil?
928
1054
  emit_row([], &block) unless @skip_blanks
929
1055
  else
930
1056
  row << value
931
- emit_row(row, &block)
1057
+ quoted_fields << @quoted_column_value
1058
+ emit_row(row, quoted_fields, &block)
932
1059
  row = []
1060
+ quoted_fields.clear
933
1061
  end
934
1062
  skip_needless_lines
935
1063
  start_row
936
1064
  elsif @scanner.eos?
937
1065
  break if row.empty? and value.nil?
938
1066
  row << value
939
- emit_row(row, &block)
1067
+ quoted_fields << @quoted_column_value
1068
+ emit_row(row, quoted_fields, &block)
940
1069
  break
941
1070
  else
942
1071
  if @quoted_column_value
1072
+ if liberal_parsing? and (new_line = @scanner.check(@line_end))
1073
+ message =
1074
+ "Illegal end-of-line sequence outside of a quoted field " +
1075
+ "<#{new_line.inspect}>"
1076
+ else
1077
+ message = "Any value after quoted field isn't allowed"
1078
+ end
943
1079
  ignore_broken_line
944
- message = "Any value after quoted field isn't allowed"
945
1080
  raise MalformedCSVError.new(message, @lineno)
946
1081
  elsif @unquoted_column_value and
947
1082
  (new_line = @scanner.scan(@line_end))
@@ -1034,7 +1169,7 @@ class CSV
1034
1169
  if (n_quotes % 2).zero?
1035
1170
  quotes[0, (n_quotes - 2) / 2]
1036
1171
  else
1037
- value = quotes[0, (n_quotes - 1) / 2]
1172
+ value = quotes[0, n_quotes / 2]
1038
1173
  while true
1039
1174
  quoted_value = @scanner.scan_all(@quoted_value)
1040
1175
  value << quoted_value if quoted_value
@@ -1058,11 +1193,9 @@ class CSV
1058
1193
  n_quotes = quotes.size
1059
1194
  if n_quotes == 1
1060
1195
  break
1061
- elsif (n_quotes % 2) == 1
1062
- value << quotes[0, (n_quotes - 1) / 2]
1063
- break
1064
1196
  else
1065
1197
  value << quotes[0, n_quotes / 2]
1198
+ break if (n_quotes % 2) == 1
1066
1199
  end
1067
1200
  end
1068
1201
  value
@@ -1098,18 +1231,15 @@ class CSV
1098
1231
 
1099
1232
  def strip_value(value)
1100
1233
  return value unless @strip
1101
- return nil if value.nil?
1234
+ return value if value.nil?
1102
1235
 
1103
1236
  case @strip
1104
1237
  when String
1105
- size = value.size
1106
- while value.start_with?(@strip)
1107
- size -= 1
1108
- value = value[1, size]
1238
+ while value.delete_prefix!(@strip)
1239
+ # do nothing
1109
1240
  end
1110
- while value.end_with?(@strip)
1111
- size -= 1
1112
- value = value[0, size]
1241
+ while value.delete_suffix!(@strip)
1242
+ # do nothing
1113
1243
  end
1114
1244
  else
1115
1245
  value.strip!
@@ -1132,22 +1262,22 @@ class CSV
1132
1262
  @scanner.keep_start
1133
1263
  end
1134
1264
 
1135
- def emit_row(row, &block)
1265
+ def emit_row(row, quoted_fields=FieldsConverter::NO_QUOTED_FIELDS, &block)
1136
1266
  @lineno += 1
1137
1267
 
1138
1268
  raw_row = row
1139
1269
  if @use_headers
1140
1270
  if @headers.nil?
1141
- @headers = adjust_headers(row)
1271
+ @headers = adjust_headers(row, quoted_fields)
1142
1272
  return unless @return_headers
1143
1273
  row = Row.new(@headers, row, true)
1144
1274
  else
1145
1275
  row = Row.new(@headers,
1146
- @fields_converter.convert(raw_row, @headers, @lineno))
1276
+ @fields_converter.convert(raw_row, @headers, @lineno, quoted_fields))
1147
1277
  end
1148
1278
  else
1149
1279
  # convert fields, if needed...
1150
- row = @fields_converter.convert(raw_row, nil, @lineno)
1280
+ row = @fields_converter.convert(raw_row, nil, @lineno, quoted_fields)
1151
1281
  end
1152
1282
 
1153
1283
  # inject unconverted fields and accessor, if requested...