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.
- checksums.yaml +4 -4
- data/NEWS.md +324 -5
- data/README.md +2 -2
- data/doc/csv/options/generating/write_headers.rdoc +1 -1
- data/doc/csv/options/parsing/liberal_parsing.rdoc +21 -2
- data/doc/csv/recipes/filtering.rdoc +85 -17
- data/doc/csv/recipes/generating.rdoc +2 -2
- data/doc/csv/recipes/parsing.rdoc +14 -5
- data/lib/csv/core_ext/array.rb +1 -1
- data/lib/csv/core_ext/string.rb +1 -1
- data/lib/csv/fields_converter.rb +10 -2
- data/lib/csv/input_record_separator.rb +1 -14
- data/lib/csv/parser.rb +238 -108
- data/lib/csv/row.rb +1 -1
- data/lib/csv/table.rb +15 -6
- data/lib/csv/version.rb +1 -1
- data/lib/csv/writer.rb +4 -5
- data/lib/csv.rb +256 -44
- metadata +7 -67
- data/lib/csv/delete_suffix.rb +0 -18
- data/lib/csv/match_p.rb +0 -20
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
209
|
-
|
|
275
|
+
def trace(*args)
|
|
276
|
+
pp([*args, @scanner, @scanner&.string, @scanner&.pos, @keeps])
|
|
277
|
+
end
|
|
210
278
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
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
|
-
@
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
425
|
+
__send__(@parse_method, &block)
|
|
426
|
+
rescue InvalidEncoding
|
|
427
|
+
if @scanner
|
|
428
|
+
ignore_broken_line
|
|
429
|
+
lineno = @lineno
|
|
337
430
|
else
|
|
338
|
-
|
|
431
|
+
lineno = @lineno + 1
|
|
339
432
|
end
|
|
340
|
-
|
|
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 = "
|
|
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
|
-
@
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
842
|
+
def gets(*args)
|
|
843
|
+
@io.gets(*args)
|
|
844
|
+
end
|
|
742
845
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
846
|
+
def each_line(*args, &block)
|
|
847
|
+
@io.each_line(*args, &block)
|
|
848
|
+
end
|
|
746
849
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
end
|
|
850
|
+
def eof?
|
|
851
|
+
@io.eof?
|
|
750
852
|
end
|
|
853
|
+
end
|
|
751
854
|
|
|
752
|
-
|
|
753
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
1234
|
+
return value if value.nil?
|
|
1102
1235
|
|
|
1103
1236
|
case @strip
|
|
1104
1237
|
when String
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
size -= 1
|
|
1108
|
-
value = value[1, size]
|
|
1238
|
+
while value.delete_prefix!(@strip)
|
|
1239
|
+
# do nothing
|
|
1109
1240
|
end
|
|
1110
|
-
while value.
|
|
1111
|
-
|
|
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...
|