calcsheet 0.1.0

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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +9 -0
  3. data/CHANGELOG.md +62 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +921 -0
  6. data/lib/calcsheet/book.rb +164 -0
  7. data/lib/calcsheet/cast.rb +578 -0
  8. data/lib/calcsheet/column.rb +117 -0
  9. data/lib/calcsheet/column_letter.rb +34 -0
  10. data/lib/calcsheet/configuration.rb +186 -0
  11. data/lib/calcsheet/decimal_text.rb +19 -0
  12. data/lib/calcsheet/entry_name.rb +28 -0
  13. data/lib/calcsheet/errors.rb +186 -0
  14. data/lib/calcsheet/excel_serial.rb +65 -0
  15. data/lib/calcsheet/format_detector.rb +109 -0
  16. data/lib/calcsheet/formula_protection.rb +31 -0
  17. data/lib/calcsheet/grid.rb +20 -0
  18. data/lib/calcsheet/header_keys.rb +66 -0
  19. data/lib/calcsheet/limited_inflater.rb +326 -0
  20. data/lib/calcsheet/merge_check.rb +41 -0
  21. data/lib/calcsheet/odf.rb +45 -0
  22. data/lib/calcsheet/option_validation.rb +160 -0
  23. data/lib/calcsheet/options.rb +10 -0
  24. data/lib/calcsheet/raw_row.rb +18 -0
  25. data/lib/calcsheet/raw_types.rb +15 -0
  26. data/lib/calcsheet/readers/csv_dialect.rb +264 -0
  27. data/lib/calcsheet/readers/csv_input.rb +128 -0
  28. data/lib/calcsheet/readers/csv_reader.rb +239 -0
  29. data/lib/calcsheet/readers/ods/cell_formats.rb +115 -0
  30. data/lib/calcsheet/readers/ods/content_parser.rb +594 -0
  31. data/lib/calcsheet/readers/ods/count.rb +30 -0
  32. data/lib/calcsheet/readers/ods/cursor.rb +121 -0
  33. data/lib/calcsheet/readers/ods/patterns.rb +70 -0
  34. data/lib/calcsheet/readers/ods/row_seek.rb +157 -0
  35. data/lib/calcsheet/readers/ods/scanner.rb +302 -0
  36. data/lib/calcsheet/readers/ods/style_scan.rb +108 -0
  37. data/lib/calcsheet/readers/ods/tag_text.rb +76 -0
  38. data/lib/calcsheet/readers/ods_reader.rb +71 -0
  39. data/lib/calcsheet/readers/ox_errors.rb +40 -0
  40. data/lib/calcsheet/readers/references.rb +62 -0
  41. data/lib/calcsheet/readers/row_batches.rb +45 -0
  42. data/lib/calcsheet/readers/sax_sheet.rb +108 -0
  43. data/lib/calcsheet/readers/stream_remainder.rb +66 -0
  44. data/lib/calcsheet/readers/tag_scan.rb +119 -0
  45. data/lib/calcsheet/readers/xlsx/escapes.rb +36 -0
  46. data/lib/calcsheet/readers/xlsx/local_name.rb +54 -0
  47. data/lib/calcsheet/readers/xlsx/number_format.rb +89 -0
  48. data/lib/calcsheet/readers/xlsx/row_skipper.rb +162 -0
  49. data/lib/calcsheet/readers/xlsx/shared_strings.rb +209 -0
  50. data/lib/calcsheet/readers/xlsx/sheet_parser.rb +435 -0
  51. data/lib/calcsheet/readers/xlsx/styles.rb +156 -0
  52. data/lib/calcsheet/readers/xlsx/workbook_meta.rb +228 -0
  53. data/lib/calcsheet/readers/xlsx_reader.rb +122 -0
  54. data/lib/calcsheet/reading.rb +281 -0
  55. data/lib/calcsheet/row.rb +110 -0
  56. data/lib/calcsheet/sample.rb +10 -0
  57. data/lib/calcsheet/schema.rb +140 -0
  58. data/lib/calcsheet/sheet_info.rb +21 -0
  59. data/lib/calcsheet/sheet_names.rb +114 -0
  60. data/lib/calcsheet/sheet_selection.rb +58 -0
  61. data/lib/calcsheet/source.rb +263 -0
  62. data/lib/calcsheet/stream_body.rb +81 -0
  63. data/lib/calcsheet/time_zone.rb +96 -0
  64. data/lib/calcsheet/untrusted.rb +32 -0
  65. data/lib/calcsheet/version.rb +6 -0
  66. data/lib/calcsheet/write_book.rb +202 -0
  67. data/lib/calcsheet/write_sheet.rb +282 -0
  68. data/lib/calcsheet/writers/csv_writer.rb +138 -0
  69. data/lib/calcsheet/writers/ods_writer.rb +437 -0
  70. data/lib/calcsheet/writers/xlsx_writer.rb +350 -0
  71. data/lib/calcsheet/writers/xml_text.rb +35 -0
  72. data/lib/calcsheet/writers/zip_buffered.rb +55 -0
  73. data/lib/calcsheet/zip_container.rb +190 -0
  74. data/lib/calcsheet/zip_directory.rb +202 -0
  75. data/lib/calcsheet/zip_end_record.rb +117 -0
  76. data/lib/calcsheet.rb +295 -0
  77. metadata +200 -0
@@ -0,0 +1,594 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ox"
4
+ require_relative "../../cast"
5
+ require_relative "../../configuration"
6
+ require_relative "../../errors"
7
+ require_relative "../../grid"
8
+ require_relative "../../odf"
9
+ require_relative "../../untrusted"
10
+ require_relative "../references"
11
+ require_relative "../sax_sheet"
12
+ require_relative "count"
13
+
14
+ module Calcsheet
15
+ module Readers
16
+ module Ods
17
+ # Streams one table of an ODS +content.xml+ with Ox's SAX parser and hands each non-blank row to a block as a
18
+ # RawRow, cast with a Cast. The stream starts at the table start tag or at a row start tag inside
19
+ # it (see Scanner#io_at_sheet); parsing stops at the end of the table.
20
+ #
21
+ # Rows are numbered by counting +table-row+ elements with their +number-rows-repeated+, cells placed by
22
+ # counting +table-cell+ and +covered-table-cell+ elements with their +number-columns-repeated+.
23
+ # LibreOffice ends a sheet with huge runs of empty repeated rows and cells: a run is counted, never expanded,
24
+ # unless it holds content. Since the file is untrusted, a repeat count that is not a positive number and content
25
+ # beyond column XFD or row 1048576 raise MalformedFileError. A repeated cell with content puts the same value
26
+ # object in each of its columns, and the rows of a repeated row share their value objects (each row has its own
27
+ # +values+ Array): copying them would multiply the memory of a long run. Changing a value in place changes it
28
+ # in every row and column of the run.
29
+ #
30
+ # Values come from +office:value+ (float, percentage, currency), +office:date-value+, +office:time-value+ and
31
+ # +office:boolean-value+, so numbers are exact; strings from +office:string-value+ or the text. The text joins
32
+ # the +text:p+ paragraphs of the cell with "\n", expands +text:s+, +text:tab+ and +text:line-break+, keeps
33
+ # the text of spans and links, and collapses other white space as ODF does: a run of spaces, tabs and line
34
+ # feeds is one space, dropped at the start of a paragraph and after another space. Elements of other
35
+ # namespaces inside a paragraph (annotations, drawings) are ignored, and so is everything in a cell that is not
36
+ # one of its paragraphs. Covered (merged) cells read as nil; LibreOffice's +calcext:value-type="error"+ marks
37
+ # an error cell, read as nil with a :cell_error.
38
+ #
39
+ # The cell style decides how a number or a date reads, as the number format does in XLSX: a date value
40
+ # under a style that shows a time is a datetime (LibreOffice writes a datetime at midnight without its time), one
41
+ # under a style that shows no time a date, and a number under a plain zero-padding style (+00000+) gets its
42
+ # zeros back as a string. The style is the cell's own, else the default cell style of its row, else that of its
43
+ # column; +formats+ (see Scanner#cell_formats) gives the format of each style that has one. Without a known
44
+ # style a date value is a datetime when it has a time.
45
+ #
46
+ # The text of a cell, its +office:string-value+ included, is at most +max_cell_bytes+ long, and a number, date,
47
+ # time or boolean value at most Cast::MAX_NUMBER_BYTES: longer ones raise MalformedFileError before they are
48
+ # joined or cast.
49
+ #
50
+ # The references of a text and of the attribute values the parser reads (+&+, +A+) are decoded by the
51
+ # gem (see References): one to NUL or an invalid code point, which Ox would truncate or change, an unknown entity
52
+ # and a stray "&" raise MalformedFileError. A CDATA section is taken as it is.
53
+ #
54
+ # Elements and attributes are matched by their qualified names, built from the prefixes of the document.
55
+ #
56
+ # @api private
57
+ class ContentParser < Ox::Sax
58
+ include SaxSheet
59
+
60
+ # skip: :skip_off hands over every whitespace-only text, also the one before the end tag of an element with
61
+ # children ("x<text:span>a</text:span> </text:p>"), which :skip_none drops though ODF keeps it as one space.
62
+ # Ox then reports the whitespace of a childless element (+<text:span> </text:span>+) twice; the collapsing of
63
+ # #text drops the second report, which follows a space. symbolize without strip_namespace: element and
64
+ # attribute names come as Symbols with their prefix (+:"table:table-row"+), the keys of +@elements+ and
65
+ # +@attributes+, built for the prefixes of the document (#name_codes). convert_special: false, so that the
66
+ # references reach #text and #attr as written (see References).
67
+ OPTIONS = { skip: :skip_off, convert_special: false, symbolize: true }.freeze
68
+ # What #text collapses: a tab, a line feed, a carriage return or two spaces.
69
+ COLLAPSIBLE = /[\t\n\r]| {2}/
70
+ # The byte of a space.
71
+ SPACE_BYTE = 32
72
+ # The kind of value of each office:value-type; another type is read as text.
73
+ KINDS = { "float" => :number, "percentage" => :number, "currency" => :number, "date" => :date, "time" => :time,
74
+ "boolean" => :boolean, "string" => :string }.freeze
75
+ # The reason of a typed cell without its value attribute.
76
+ MISSING = { date: :invalid_date, boolean: :invalid_boolean }.freeze
77
+
78
+ # Element codes, the values of +@elements+: a Hash lookup and an Integer +case+ per callback.
79
+ TABLE = 1
80
+ ROW = 2
81
+ CELL = 3
82
+ COVERED = 4
83
+ PARAGRAPH = 5
84
+ SPACES = 6
85
+ TAB = 7
86
+ LINE_BREAK = 8
87
+ COLUMN = 9
88
+ # Attribute codes, the values of +@attributes+.
89
+ VALUE_TYPE = 1
90
+ VALUE = 2
91
+ DATE_VALUE = 3
92
+ TIME_VALUE = 4
93
+ BOOLEAN_VALUE = 5
94
+ STRING_VALUE = 6
95
+ CALC_VALUE_TYPE = 7
96
+ COLUMNS_REPEATED = 8
97
+ ROWS_REPEATED = 9
98
+ SPACE_COUNT = 10
99
+ STYLE_NAME = 11
100
+ DEFAULT_STYLE = 12
101
+ # The +formats:+ of a document without cell formats.
102
+ NO_FORMATS = {}.freeze
103
+ # The attribute that holds the value of each kind.
104
+ VALUE_OF = { number: VALUE, date: DATE_VALUE, time: TIME_VALUE, boolean: BOOLEAN_VALUE, string: STRING_VALUE }.freeze
105
+
106
+ # Parses +io+ and yields the rows numbered +start_row+ or later.
107
+ #
108
+ # @param io [#readpartial] the table from its start tag, or from a row start tag (see Scanner::Cut)
109
+ # @param sheet [String] the sheet name, for errors
110
+ # @param prefixes [Hash{Symbol => String}] the prefixes of the office, table, text and calcext namespaces
111
+ # @param cast [Cast]
112
+ # @param types [#[], nil] the declared type per column index (see Reading)
113
+ # @param first_row [Integer] the number of the first row in +io+
114
+ # @param start_row [Integer]
115
+ # @param formats [Hash{String => Array(Symbol, Integer | nil)}] +[kind, pad]+ by cell style name
116
+ # @param max_cell_bytes [Integer, nil] the longest text of a cell; nil for no limit
117
+ # @yieldparam row [RawRow]
118
+ # @raise [MalformedFileError] for damaged XML (the parser error is the +cause+), an invalid repeat count,
119
+ # content beyond the last row or column, text that is not UTF-8 or too long, or another error of the stream
120
+ # (kept as +cause+)
121
+ # @raise [Calcsheet::Error] an error of the stream itself (a zip limit)
122
+ def self.parse(io, sheet:, prefixes:, cast:, types:, first_row: 1, start_row: 1, formats: NO_FORMATS,
123
+ max_cell_bytes: Configuration::DEFAULTS[:max_cell_bytes], &block)
124
+ new(sheet: sheet, prefixes: prefixes, cast: cast, types: types, first_row: first_row, start_row: start_row,
125
+ formats: formats, max_cell_bytes: max_cell_bytes, on_row: block).parse(io)
126
+ end
127
+
128
+ # @api private
129
+ #
130
+ # The state of the parse, in instance variables so that a callback allocates nothing:
131
+ #
132
+ # - set once: +@sheet+, +@cast+, +@types+, +@formats+ (nil when empty), +@start_row+, +@on_row+,
133
+ # +@max_cell_bytes+, and from #name_codes +@elements+, +@attributes+, +@text_qualifier+,
134
+ # +@rows_repeated_name+ and +@columns_repeated_name+;
135
+ # - across rows: +@next_row+ (the number of the next row), +@columns+ (the columns of the table-column elements
136
+ # so far) and +@column_formats+ (+[first, last, format]+ of those whose default cell style has a format),
137
+ # +@yielding+ (see SaxSheet#parse);
138
+ # - the element whose attributes come: +@element+ (an element code, nil inside a cell but for text:s, text:tab
139
+ # and text:line-break);
140
+ # - per table-column: +@column_repeated+, +@column_style+;
141
+ # - per row: +@in_row+, +@row_number+, +@rows_repeated+, +@row_style+, +@values+ (sparse, see
142
+ # SaxSheet#trim_row!), +@errors+, +@last+ and +@column+ (the column of the next cell);
143
+ # - per cell: +@in_cell+, +@covered+, +@depth+ (below the cell element), +@columns_repeated+, +@kind+ (from the
144
+ # value type), +@value+ and +@value_code+ (the value attribute and which one), +@cell_style+, +@error+
145
+ # (+calcext:value-type="error"+), +@missing+ (the reason of a typed cell without its value), +@text+,
146
+ # +@paragraphs+, +@paragraph+ (inside one), +@capture+ (the text is the value), +@ignored+ (the depth of an
147
+ # element of another namespace), +@spaces+ (the text:s spaces so far), +@space_count+ (the +text:c+ of the
148
+ # current text:s) and +@ignore_space+ (a space is dropped here: at the start of a paragraph, after a space).
149
+ def initialize(sheet:, prefixes:, cast:, types:, first_row:, start_row:, formats:, max_cell_bytes:, on_row:)
150
+ super()
151
+ @max_cell_bytes = max_cell_bytes
152
+ @sheet = sheet
153
+ @cast = cast
154
+ @types = types
155
+ @formats = formats.empty? ? nil : formats
156
+ # [first column, last column, format] of the columns whose default cell style has a format.
157
+ @column_formats = []
158
+ @columns = 0
159
+ @next_row = first_row
160
+ @start_row = start_row
161
+ @on_row = on_row
162
+ name_codes(prefixes)
163
+ @yielding = false
164
+ @element = nil
165
+ @in_row = false
166
+ @in_cell = false
167
+ @row_number = nil
168
+ end
169
+
170
+ # @api private
171
+ # Inside a cell only its paragraphs count: +@depth+ is the depth below the cell element.
172
+ def start_element(name)
173
+ if @in_cell
174
+ @depth += 1
175
+ @element = nil
176
+ if @paragraph then start_in_paragraph(name)
177
+ elsif @depth == 1 && @elements[name] == PARAGRAPH then begin_paragraph
178
+ end
179
+ return
180
+ end
181
+
182
+ @element = code = @elements[name]
183
+ case code
184
+ when ROW then begin_row
185
+ when CELL, COVERED then begin_cell(code) if @in_row
186
+ when COLUMN then begin_column
187
+ end
188
+ end
189
+
190
+ # @api private
191
+ def end_element(name)
192
+ if @in_cell
193
+ return finish_cell if @depth.zero?
194
+
195
+ end_in_paragraph if @paragraph
196
+ @element = nil
197
+ @depth -= 1
198
+ return
199
+ end
200
+
201
+ case @elements[name]
202
+ when ROW then finish_row if @in_row
203
+ when COLUMN then finish_column
204
+ when TABLE then throw STOP
205
+ end
206
+ end
207
+
208
+ # @api private
209
+ # The attributes the parser reads have their references decoded where they are used: a value type when it is
210
+ # not a known one, the value of a cell when the cell ends (#raw_value), the others here. An attribute of
211
+ # another element than the one it belongs to is ignored, as are those of the table (Scanner reads its name).
212
+ def attr(name, value)
213
+ case @element
214
+ when CELL
215
+ case (code = @attributes[name])
216
+ when VALUE_TYPE then @kind = KINDS.fetch(value) { KINDS.fetch(plain(name, value), :text) }
217
+ when VALUE, DATE_VALUE, TIME_VALUE, BOOLEAN_VALUE, STRING_VALUE
218
+ @value = value
219
+ @value_code = code
220
+ when CALC_VALUE_TYPE then @error = value == "error" || (value.include?("&") && plain(name, value) == "error")
221
+ when COLUMNS_REPEATED then @columns_repeated = repeat(plain(name, value), @columns_repeated_name)
222
+ when STYLE_NAME then @cell_style = value.include?("&") ? plain(name, value) : value
223
+ end
224
+ when COVERED
225
+ @columns_repeated = repeat(plain(name, value), @columns_repeated_name) if @attributes[name] == COLUMNS_REPEATED
226
+ when ROW
227
+ case @attributes[name]
228
+ when ROWS_REPEATED then @rows_repeated = repeat(plain(name, value), @rows_repeated_name)
229
+ when DEFAULT_STYLE then @row_style = plain(name, value)
230
+ end
231
+ when COLUMN
232
+ case @attributes[name]
233
+ when COLUMNS_REPEATED then @column_repeated = repeat(plain(name, value), @columns_repeated_name)
234
+ when DEFAULT_STYLE then @column_style = plain(name, value)
235
+ end
236
+ when SPACES
237
+ @space_count = plain(name, value) if @attributes[name] == SPACE_COUNT
238
+ end
239
+ end
240
+
241
+ # @api private
242
+ # The text of a paragraph, its references decoded unless it is a CDATA section, collapsed as ODF collapses
243
+ # white space.
244
+ def text(value, references = true)
245
+ return unless @capture && @paragraph && @ignored.nil?
246
+
247
+ value = text_references(value) if references && value.include?("&")
248
+ raise malformed("the text is not valid UTF-8", column_index: @column) unless value.valid_encoding?
249
+
250
+ if (@ignore_space && white?(value.getbyte(0))) || COLLAPSIBLE.match?(value)
251
+ # Like gsub(/[ \t\r\n]+/, " "), without the memory a regexp run costs per byte of a long one.
252
+ value = value.tr("\t\r\n", " ").squeeze(" ")
253
+ value = value.byteslice(1, value.bytesize - 1) if @ignore_space && value.getbyte(0) == SPACE_BYTE
254
+ return if value.empty?
255
+ end
256
+ @ignore_space = value.getbyte(-1) == SPACE_BYTE
257
+ append(value)
258
+ end
259
+
260
+ # @api private
261
+ # A CDATA section of a paragraph, taken as it is.
262
+ def cdata(value)
263
+ text(value, false)
264
+ end
265
+
266
+ private
267
+
268
+ def text_references(value)
269
+ References.decode(value)
270
+ rescue References::Invalid => error
271
+ raise malformed("the text holds #{error.message}", column_index: @column)
272
+ end
273
+
274
+ # The value of an attribute with its references decoded. The column is the cell's (or the next cell's, for a
275
+ # row or a table-column).
276
+ def plain(name, value)
277
+ return value unless value.include?("&")
278
+
279
+ References.decode(value)
280
+ rescue References::Invalid => error
281
+ raise malformed("the attribute #{name} holds #{error.message}", column_index: (@column if @in_row))
282
+ end
283
+
284
+ def name_codes(prefixes)
285
+ table = Odf.qualifier(prefixes[:table])
286
+ office = Odf.qualifier(prefixes[:office])
287
+ text = Odf.qualifier(prefixes[:text])
288
+ calcext = Odf.qualifier(prefixes[:calcext])
289
+ @text_qualifier = text
290
+ @elements = {
291
+ "#{table}table": TABLE, "#{table}table-row": ROW, "#{table}table-cell": CELL,
292
+ "#{table}covered-table-cell": COVERED, "#{table}table-column": COLUMN, "#{text}p": PARAGRAPH, "#{text}s": SPACES,
293
+ "#{text}tab": TAB, "#{text}line-break": LINE_BREAK
294
+ }
295
+ @attributes = {
296
+ "#{table}number-rows-repeated": ROWS_REPEATED, "#{table}number-columns-repeated": COLUMNS_REPEATED,
297
+ "#{office}value-type": VALUE_TYPE, "#{office}value": VALUE, "#{office}date-value": DATE_VALUE,
298
+ "#{office}time-value": TIME_VALUE, "#{office}boolean-value": BOOLEAN_VALUE,
299
+ "#{office}string-value": STRING_VALUE, "#{calcext}value-type": CALC_VALUE_TYPE, "#{text}c": SPACE_COUNT,
300
+ "#{table}style-name": STYLE_NAME, "#{table}default-cell-style-name": DEFAULT_STYLE
301
+ }
302
+ @value_names = @attributes.invert.slice(VALUE, DATE_VALUE, TIME_VALUE, BOOLEAN_VALUE, STRING_VALUE)
303
+ @rows_repeated_name = Odf.qualified(prefixes[:table], "number-rows-repeated")
304
+ @columns_repeated_name = Odf.qualified(prefixes[:table], "number-columns-repeated")
305
+ end
306
+
307
+ # --- columns --------------------------------------------------------------------------
308
+
309
+ def begin_column
310
+ @column_repeated = 1
311
+ @column_style = nil
312
+ end
313
+
314
+ # Notes the columns of the element when their default cell style has a format; columns beyond the last one
315
+ # cannot hold cells.
316
+ def finish_column
317
+ first = @columns
318
+ @columns += @column_repeated
319
+ return unless @formats && @column_style && first < Grid::MAX_COLUMNS
320
+
321
+ format = @formats[@column_style] or return
322
+ @column_formats << [ first, @columns - 1, format ]
323
+ end
324
+
325
+ # @return [Array(Symbol, Integer | nil), nil] the format of the default cell style of the column
326
+ def column_format(column)
327
+ range = @column_formats.bsearch { |entry| entry[1] >= column }
328
+ range[2] if range && range[0] <= column
329
+ end
330
+
331
+ # @return [Array(Symbol, Integer | nil), nil] the format of the cell at +column+: of its own style, else of the
332
+ # default style of its row, else of its column
333
+ def format_at(column)
334
+ return unless @formats
335
+
336
+ style = @cell_style || @row_style
337
+ style ? @formats[style] : column_format(column)
338
+ end
339
+
340
+ # --- rows -----------------------------------------------------------------------------
341
+
342
+ def begin_row
343
+ @in_row = true
344
+ @row_number = @next_row
345
+ @row_style = nil
346
+ @rows_repeated = 1
347
+ @values = []
348
+ @errors = nil
349
+ @last = -1
350
+ @column = 0
351
+ end
352
+
353
+ def finish_row
354
+ @in_row = false
355
+ number = @row_number
356
+ last_number = number + @rows_repeated - 1
357
+ @next_row = last_number + 1
358
+ return if @last.negative? || last_number < @start_row
359
+ raise malformed("the row is beyond the last row #{Grid::MAX_ROWS}", row: number) if last_number > Grid::MAX_ROWS
360
+
361
+ trim_row!
362
+ [ number, @start_row ].max.upto(last_number) { |row| emit(row, last: row == last_number) }
363
+ end
364
+
365
+ # Yields the row numbered +number+; every row of a repeated run but the last gets copies of the arrays.
366
+ def emit(number, last:)
367
+ yield_row(number, last ? @values : @values.dup, last || @errors.nil? ? @errors : @errors.dup)
368
+ end
369
+
370
+ # --- cells ----------------------------------------------------------------------------
371
+
372
+ def begin_cell(code)
373
+ @in_cell = true
374
+ @covered = code == COVERED
375
+ @depth = 0
376
+ @columns_repeated = 1
377
+ @kind = :text
378
+ @value = @value_code = nil
379
+ @cell_style = nil
380
+ @error = false
381
+ @text = nil
382
+ @paragraphs = 0
383
+ @paragraph = false
384
+ @capture = false
385
+ @ignored = nil
386
+ @spaces = 0
387
+ end
388
+
389
+ # An element inside a paragraph. +@ignored+ is the depth of an element of another namespace, whose content
390
+ # is skipped.
391
+ def start_in_paragraph(name)
392
+ return if @ignored
393
+
394
+ code = @elements[name]
395
+ if code == SPACES || code == TAB || code == LINE_BREAK
396
+ @element = code
397
+ @space_count = nil
398
+ elsif code.nil? && !text_element?(name)
399
+ @ignored = @depth
400
+ end
401
+ end
402
+
403
+ def end_in_paragraph
404
+ if @ignored
405
+ @ignored = nil if @ignored == @depth
406
+ elsif @depth == 1
407
+ @paragraph = false
408
+ elsif @capture
409
+ control(@element)
410
+ end
411
+ end
412
+
413
+ def text_element?(name)
414
+ @text_qualifier.empty? || name.start_with?(@text_qualifier)
415
+ end
416
+
417
+ def begin_paragraph
418
+ @paragraph = true
419
+ @capture = wants_text? if @paragraphs.zero?
420
+ @paragraphs += 1
421
+ return unless @capture
422
+
423
+ append(+"\n") if @paragraphs > 1
424
+ @ignore_space = true
425
+ end
426
+
427
+ # Whether the value of the cell is its text: no value attribute of its kind.
428
+ def wants_text?
429
+ !@covered && (@error || @value_code.nil? || @value_code != VALUE_OF[@kind])
430
+ end
431
+
432
+ # The end of a text:s, text:tab or text:line-break element of a paragraph.
433
+ def control(code)
434
+ case code
435
+ when SPACES
436
+ count = Count.parse(@space_count) || 1
437
+ @spaces += count
438
+ raise malformed("the text has more than #{Odf::MAX_SPACES} spaces", column_index: @column) if @spaces > Odf::MAX_SPACES
439
+
440
+ append(" " * count)
441
+ when TAB then append(+"\t")
442
+ when LINE_BREAK then append(+"\n")
443
+ else return
444
+ end
445
+ @ignore_space = false
446
+ end
447
+
448
+ # The white space of XML: space, tab (9), line feed (10) and carriage return (13).
449
+ def white?(byte)
450
+ byte == SPACE_BYTE || byte == 9 || byte == 10 || byte == 13
451
+ end
452
+
453
+ # Measured before it is joined: a text longer than max_cell_bytes is never built.
454
+ def append(text)
455
+ if @text
456
+ raise text_too_long if @max_cell_bytes && @text.bytesize + text.bytesize > @max_cell_bytes
457
+
458
+ @text << text
459
+ else
460
+ raise text_too_long if @max_cell_bytes && text.bytesize > @max_cell_bytes
461
+
462
+ @text = text
463
+ end
464
+ end
465
+
466
+ def text_too_long
467
+ malformed("the text is longer than max_cell_bytes (#{@max_cell_bytes} bytes)", column_index: @column)
468
+ end
469
+
470
+ def finish_cell
471
+ @in_cell = false
472
+ column = @column
473
+ raw = raw_value unless @covered
474
+ repeat = @columns_repeated
475
+ @column = column + repeat
476
+ return if raw.nil?
477
+
478
+ raise malformed("a cell is beyond the last column #{Grid::LAST_COLUMN}") if @column > Grid::MAX_COLUMNS
479
+
480
+ if repeat == 1
481
+ begin
482
+ @values[column] = cast(raw, @types && @types[column], format_of(column))
483
+ rescue Cast::Failure => failure
484
+ (@errors ||= []) << [ column, raw, failure.reason ]
485
+ end
486
+ else
487
+ place(raw, column)
488
+ end
489
+ @last = @column - 1 if (@kind != :text || !blank?(raw)) && @column - 1 > @last
490
+ end
491
+
492
+ # Casts the value of a repeated cell into each of its columns, once per declared type and format.
493
+ def place(raw, column)
494
+ type = format = value = failure = nil
495
+ column.upto(@column - 1) do |index|
496
+ wanted = @types && @types[index]
497
+ styled = format_of(index)
498
+ # Identity, not equality: the formats are the frozen Arrays of the +formats+ Hash, so the same style gives
499
+ # the same object, and a check by identity costs nothing per column. An equal format of another style only
500
+ # casts once more.
501
+ if index == column || wanted != type || !styled.equal?(format)
502
+ type = wanted
503
+ format = styled
504
+ begin
505
+ failure = nil
506
+ value = cast(raw, wanted, styled)
507
+ rescue Cast::Failure => error
508
+ failure = error
509
+ end
510
+ end
511
+ if failure then (@errors ||= []) << [ index, raw, failure.reason ]
512
+ else @values[index] = value
513
+ end
514
+ end
515
+ end
516
+
517
+ # The raw value of the cell; leaves its kind in +@kind+: :text (strings), :number, :date, :time, :boolean,
518
+ # :error, or :missing for a typed cell without its value attribute.
519
+ # @return [String, nil] nil for an empty cell
520
+ def raw_value
521
+ kind = @kind
522
+ if @error
523
+ @kind = :error
524
+ utf8(@text || "")
525
+ elsif @value_code && @value_code == VALUE_OF[kind]
526
+ value = @value.include?("&") ? plain(@value_names[@value_code], @value) : @value
527
+ return utf8(number_value(value)) unless kind == :string
528
+
529
+ @kind = :text
530
+ raise text_too_long if @max_cell_bytes && value.bytesize > @max_cell_bytes
531
+
532
+ utf8(value) unless value.empty?
533
+ elsif kind == :text || kind == :string
534
+ @kind = :text
535
+ utf8(@text) if @text && !@text.empty?
536
+ else
537
+ @missing = MISSING.fetch(kind, :invalid_decimal)
538
+ @kind = :missing
539
+ utf8(@text || "")
540
+ end
541
+ end
542
+
543
+ def number_value(value)
544
+ return value if value.bytesize <= Cast::MAX_NUMBER_BYTES
545
+
546
+ raise malformed("the value is longer than #{Cast::MAX_NUMBER_BYTES} bytes", column_index: @column)
547
+ end
548
+
549
+ # The format only matters to numbers and dates.
550
+ def format_of(column)
551
+ format_at(column) if @kind == :number || @kind == :date
552
+ end
553
+
554
+ def cast(raw, type, format)
555
+ case @kind
556
+ when :text then @cast.text(raw, type)
557
+ when :number then @cast.number(raw, :general, type, pad: format && format[1])
558
+ when :date then @cast.iso_date(raw, type, format && format[0])
559
+ when :time then @cast.duration(raw, type)
560
+ when :boolean then @cast.boolean(boolean(raw), type)
561
+ when :error then raise Cast::Failure.new(:cell_error, raw)
562
+ else raise Cast::Failure.new(@missing, raw)
563
+ end
564
+ end
565
+
566
+ # Ox tags the values as UTF-8, as the stream declares; it does not check the bytes.
567
+ def utf8(text)
568
+ return text if text.valid_encoding?
569
+
570
+ raise malformed("the text is not valid UTF-8", column_index: @column)
571
+ end
572
+
573
+ def repeat(value, name)
574
+ count = Count.parse(value)
575
+ return count if count&.positive?
576
+
577
+ raise malformed("#{name} must be a positive number, got #{Untrusted.show(value)}")
578
+ end
579
+
580
+ # The row being read when it is known, else the last row read.
581
+ def current_row
582
+ return @row_number if @in_row
583
+
584
+ @next_row > 1 && @row_number ? @next_row - 1 : nil
585
+ end
586
+
587
+ # The column of the cell being read.
588
+ def current_column
589
+ @column
590
+ end
591
+ end
592
+ end
593
+ end
594
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calcsheet
4
+ module Readers
5
+ module Ods
6
+ # The counts written in ODS attributes (repeats, spaces, digits of a number style), parsed alike by Scanner
7
+ # and ContentParser, so that a file reads the same from its first row and from +start_at:+.
8
+ #
9
+ # @api private
10
+ module Count
11
+ # More digits are not a count Calcsheet can use: a repeat or a number of spaces is invalid beyond them. 18
12
+ # digits stay below 2**62, so a count is always an immediate Integer (no Bignum) and the arithmetic on row and
13
+ # column numbers stays cheap; every real count (rows, columns, spaces) has far fewer digits.
14
+ MAX_DIGITS = 18
15
+ # A byte that is not an ASCII digit.
16
+ NOT_DIGIT = /[^0-9]/
17
+
18
+ # @param value [String, nil] an attribute value
19
+ # @return [Integer, nil] the number written in +value+: ASCII digits only, at most MAX_DIGITS of them; nil
20
+ # otherwise. A search for another character, not an anchored /\A\d+\z/, whose backtracking costs memory in
21
+ # proportion to the length of the value.
22
+ def self.parse(value)
23
+ return unless value && !value.empty? && value.bytesize <= MAX_DIGITS && value.valid_encoding?
24
+
25
+ value.to_i unless NOT_DIGIT.match?(value)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end