calcsheet-native 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +36 -0
- data/Cargo.lock +283 -0
- data/Cargo.toml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +65 -0
- data/ext/calcsheet_native/Cargo.toml +14 -0
- data/ext/calcsheet_native/extconf.rb +6 -0
- data/ext/calcsheet_native/src/csv.rs +309 -0
- data/ext/calcsheet_native/src/gvl.rs +47 -0
- data/ext/calcsheet_native/src/lib.rs +470 -0
- data/ext/calcsheet_native/src/ods.rs +815 -0
- data/ext/calcsheet_native/src/xlsx.rs +814 -0
- data/ext/calcsheet_native/src/xml.rs +370 -0
- data/lib/calcsheet/native/csv_reader.rb +122 -0
- data/lib/calcsheet/native/ods_reader.rb +148 -0
- data/lib/calcsheet/native/version.rb +8 -0
- data/lib/calcsheet/native/xlsx_reader.rb +208 -0
- data/lib/calcsheet/native.rb +31 -0
- metadata +100 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Calcsheet
|
|
4
|
+
module Native
|
|
5
|
+
# The native XLSX backend: calcsheet's Ruby reader (the zip archive and its limits, the workbook, styles and
|
|
6
|
+
# shared strings) with the XML of each sheet read in Rust instead of by Ox. The Rust side reads a plain subset of
|
|
7
|
+
# XML and the values calcsheet accepts; on anything else (a comment, CDATA, a reference in an attribute, a value
|
|
8
|
+
# calcsheet refuses, an error of the zip stream...) the sheet is read again by calcsheet's own parser, which
|
|
9
|
+
# gives its exact rows or error. The rows already yielded are not yielded again, and the bytes the first attempt
|
|
10
|
+
# inflated are not charged twice against +max_uncompressed_size+.
|
|
11
|
+
#
|
|
12
|
+
# A read from a later row (the rows after the header, +start_at:+) parses the rows before it too, where calcsheet
|
|
13
|
+
# passes over them with RowSkipper: what the subset reads, RowSkipper passes over without an error, and anything
|
|
14
|
+
# else is again left to calcsheet.
|
|
15
|
+
#
|
|
16
|
+
# It depends on the private methods of Calcsheet::Readers::XlsxReader and the internals of ZipContainer of the
|
|
17
|
+
# calcsheet minor version it supports.
|
|
18
|
+
class XlsxReader < Calcsheet::Readers::XlsxReader
|
|
19
|
+
# calcsheet's SharedStrings over a StringTable read in Rust, which the sheet readers share: one copy of the
|
|
20
|
+
# strings.
|
|
21
|
+
class SharedStrings < Readers::Xlsx::SharedStrings
|
|
22
|
+
# @return [StringTable]
|
|
23
|
+
attr_reader :native
|
|
24
|
+
|
|
25
|
+
# @param table [StringTable]
|
|
26
|
+
def initialize(table) # rubocop:disable Lint/MissingSuper -- the buffer and offsets of the superclass are in Rust
|
|
27
|
+
@native = table
|
|
28
|
+
@size = table.size
|
|
29
|
+
@bytesize = table.bytesize
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @see Readers::Xlsx::SharedStrings#unchecked
|
|
33
|
+
def unchecked(index)
|
|
34
|
+
@native.unchecked(index)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Thrown when calcsheet's parser must read the sheet.
|
|
39
|
+
FALLBACK = :calcsheet_native_fallback
|
|
40
|
+
# Rows per call into Rust, and bytes read from the zip stream at a time.
|
|
41
|
+
ROWS = 512
|
|
42
|
+
CHUNK = 64 * 1024
|
|
43
|
+
# The chunks of shared strings between two minor GCs (see #read_shared_strings).
|
|
44
|
+
GC_CHUNKS = 32
|
|
45
|
+
# The kinds of XlsxSheet's cells; the text of a SHARED cell is its shared string.
|
|
46
|
+
NUMBER = 0
|
|
47
|
+
SHARED = 1
|
|
48
|
+
INLINE = 2
|
|
49
|
+
BOOLEAN = 3
|
|
50
|
+
ERROR = 4
|
|
51
|
+
DATE = 5
|
|
52
|
+
# The inflated total of ZipContainer, restored before calcsheet's parser reads the sheet again.
|
|
53
|
+
TOTAL = :@inflated_total
|
|
54
|
+
|
|
55
|
+
@fallbacks = 0
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
# @return [Integer] the sheet reads handed to calcsheet's parser in this process, for specs and diagnostics
|
|
59
|
+
attr_accessor :fallbacks
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def each_row(index, cast, types, start_row, &block)
|
|
65
|
+
path = @meta.sheet_path(index)
|
|
66
|
+
return super unless @zip.entry?(path)
|
|
67
|
+
|
|
68
|
+
charged = @zip.instance_variable_get(TOTAL)
|
|
69
|
+
strings = @shared_strings
|
|
70
|
+
last = 0
|
|
71
|
+
finished = catch(FALLBACK) do
|
|
72
|
+
read_natively(path, cast, types, start_row) do |row|
|
|
73
|
+
last = row.number
|
|
74
|
+
yield row
|
|
75
|
+
end
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
return if finished
|
|
79
|
+
|
|
80
|
+
self.class.fallbacks += 1
|
|
81
|
+
@zip.instance_variable_set(TOTAL, charged)
|
|
82
|
+
@shared_strings = strings
|
|
83
|
+
super(index, cast, types, start_row) { |row| yield row if row.number > last }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Yields the rows read by XlsxSheet; throws FALLBACK when calcsheet's parser must read the sheet. The errors of
|
|
87
|
+
# the caller's block are its own.
|
|
88
|
+
def read_natively(path, cast, types, start_row)
|
|
89
|
+
stream = guarded { @zip.stream(path) }
|
|
90
|
+
sheet = XlsxSheet.new(@max_cell_bytes)
|
|
91
|
+
builder = RowBuilder.new(cast, types, number_errors: :as_read)
|
|
92
|
+
@style_formats = {}
|
|
93
|
+
# Read into one String: a new one per chunk would leave the whole part as garbage before the GC runs.
|
|
94
|
+
buffer = String.new(capacity: CHUNK)
|
|
95
|
+
loop do
|
|
96
|
+
batch = guarded { sheet.rows(ROWS) }
|
|
97
|
+
throw FALLBACK if batch == false
|
|
98
|
+
if batch == :strings
|
|
99
|
+
guarded do
|
|
100
|
+
table = shared_strings
|
|
101
|
+
if table.is_a?(SharedStrings)
|
|
102
|
+
sheet.table(table.native)
|
|
103
|
+
else
|
|
104
|
+
sheet.strings(table.instance_variable_get(:@buffer), table.instance_variable_get(:@offsets))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
next
|
|
108
|
+
end
|
|
109
|
+
if batch.nil?
|
|
110
|
+
chunk = guarded { stream.read(CHUNK, buffer) }
|
|
111
|
+
chunk ? sheet.feed(chunk) : sheet.finish
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
numbers, ends, cells, done = batch
|
|
116
|
+
from = 0
|
|
117
|
+
numbers.each_with_index do |number, position|
|
|
118
|
+
to = ends[position]
|
|
119
|
+
guarded { add_cells(builder, cells, from, to) }
|
|
120
|
+
from = to
|
|
121
|
+
row = builder.finish(number)
|
|
122
|
+
yield row if row && number >= start_row
|
|
123
|
+
end
|
|
124
|
+
break if done
|
|
125
|
+
end
|
|
126
|
+
ensure
|
|
127
|
+
stream&.close
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Runs the block, throwing FALLBACK for any error it raises: calcsheet's parser raises it again, where it
|
|
131
|
+
# would.
|
|
132
|
+
def guarded
|
|
133
|
+
yield
|
|
134
|
+
rescue StandardError
|
|
135
|
+
throw FALLBACK
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Adds the cells +from...to+ of a batch to the builder, as SheetParser#finish_cell does.
|
|
139
|
+
def add_cells(builder, cells, from, to)
|
|
140
|
+
while from < to
|
|
141
|
+
column = cells[from]
|
|
142
|
+
style = cells[from + 2]
|
|
143
|
+
text = cells[from + 3]
|
|
144
|
+
text = read_text(text) if cells[from + 4]
|
|
145
|
+
case cells[from + 1]
|
|
146
|
+
when NUMBER then builder.add(column, :number, text, format(style))
|
|
147
|
+
when SHARED then builder.add(column, :text, text)
|
|
148
|
+
when INLINE then builder.add(column, :text, Readers::Xlsx::Escapes.decode!(text))
|
|
149
|
+
when BOOLEAN then builder.add(column, :boolean, text)
|
|
150
|
+
when ERROR then builder.add(column, :error, text)
|
|
151
|
+
when DATE then builder.add(column, :iso_date, text, format(style))
|
|
152
|
+
else builder.add(column, :text, text)
|
|
153
|
+
end
|
|
154
|
+
from += 6
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The line ends and references of a text, as SheetParser#text reads them.
|
|
159
|
+
def read_text(text)
|
|
160
|
+
text = Readers::References.line_ends(text) if text.include?("\r")
|
|
161
|
+
text.include?("&") ? Readers::References.decode(text) : text
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The +[kind, pad]+ of a number or date cell's style (SheetParser#style_kind): 0 without one.
|
|
165
|
+
def format(style)
|
|
166
|
+
return @meta.style_kind(0) if style.nil?
|
|
167
|
+
|
|
168
|
+
@style_formats[style] ||= begin
|
|
169
|
+
throw FALLBACK unless style.zero? || style.between?(1, @meta.style_count - 1)
|
|
170
|
+
@meta.style_kind(style)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The shared strings read in Rust, as calcsheet's SharedStrings; calcsheet loads them itself when the Rust side
|
|
175
|
+
# does not read them, with the inflated total set back first.
|
|
176
|
+
def load_shared_strings
|
|
177
|
+
return super unless @zip.entry?(SHARED_STRINGS)
|
|
178
|
+
|
|
179
|
+
charged = @zip.instance_variable_get(TOTAL)
|
|
180
|
+
table = catch(FALLBACK) { read_shared_strings }
|
|
181
|
+
return table if table
|
|
182
|
+
|
|
183
|
+
@zip.instance_variable_set(TOTAL, charged)
|
|
184
|
+
super
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def read_shared_strings
|
|
188
|
+
stream = guarded { @zip.stream(SHARED_STRINGS, ratio_floor: false) }
|
|
189
|
+
reader = XlsxStrings.new(@max_cell_bytes)
|
|
190
|
+
buffer = String.new(capacity: CHUNK)
|
|
191
|
+
chunks = 0
|
|
192
|
+
loop do
|
|
193
|
+
result = guarded { reader.read }
|
|
194
|
+
throw FALLBACK if result == false
|
|
195
|
+
return SharedStrings.new(result) if result
|
|
196
|
+
|
|
197
|
+
chunk = guarded { stream.read(CHUNK, buffer) }
|
|
198
|
+
chunk ? reader.feed(chunk) : reader.finish
|
|
199
|
+
# The inflater leaves its pieces as garbage, and this loop allocates too little for the GC to run: a
|
|
200
|
+
# minor GC every GC_CHUNKS keeps the peak memory at the Ruby backend's.
|
|
201
|
+
GC.start(full_mark: false) if (chunks += 1) % GC_CHUNKS == 0
|
|
202
|
+
end
|
|
203
|
+
ensure
|
|
204
|
+
stream&.close
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "calcsheet"
|
|
4
|
+
require_relative "native/version"
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
# A precompiled platform gem holds one extension per Ruby minor version; a gem built from source holds one.
|
|
8
|
+
require_relative "native/#{RUBY_VERSION[/\A\d+\.\d+/]}/calcsheet_native"
|
|
9
|
+
rescue LoadError
|
|
10
|
+
require_relative "native/calcsheet_native"
|
|
11
|
+
end
|
|
12
|
+
require_relative "native/csv_reader"
|
|
13
|
+
require_relative "native/ods_reader"
|
|
14
|
+
require_relative "native/xlsx_reader"
|
|
15
|
+
|
|
16
|
+
module Calcsheet
|
|
17
|
+
# The native reader backends of calcsheet, which Calcsheet.open finds through +require "calcsheet/native"+ (see
|
|
18
|
+
# Calcsheet::NativeBackends in calcsheet).
|
|
19
|
+
module Native
|
|
20
|
+
# The version of the interface with calcsheet: Calcsheet::NativeBackends::PROTOCOLS lists those it accepts.
|
|
21
|
+
PROTOCOL = 1
|
|
22
|
+
|
|
23
|
+
# The formats read natively.
|
|
24
|
+
BACKENDS = { csv: CsvReader, xlsx: XlsxReader, ods: OdsReader }.freeze
|
|
25
|
+
|
|
26
|
+
# @return [Hash{Symbol => Class}] the backend class of each format read natively
|
|
27
|
+
def self.backends
|
|
28
|
+
BACKENDS
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: calcsheet-native
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Demetra Opinioni.net Srl
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: calcsheet
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.2'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0.4'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '0.2'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '0.4'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: rb_sys
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 0.9.130
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 0.9.130
|
|
46
|
+
description: |
|
|
47
|
+
calcsheet-native reads CSV, XLSX and ODS for calcsheet with a Rust core that runs without the GVL, and gives
|
|
48
|
+
the same rows as calcsheet's Ruby backends.
|
|
49
|
+
email:
|
|
50
|
+
- developers@opinioni.net
|
|
51
|
+
executables: []
|
|
52
|
+
extensions:
|
|
53
|
+
- ext/calcsheet_native/extconf.rb
|
|
54
|
+
extra_rdoc_files: []
|
|
55
|
+
files:
|
|
56
|
+
- CHANGELOG.md
|
|
57
|
+
- Cargo.lock
|
|
58
|
+
- Cargo.toml
|
|
59
|
+
- LICENSE.txt
|
|
60
|
+
- README.md
|
|
61
|
+
- ext/calcsheet_native/Cargo.toml
|
|
62
|
+
- ext/calcsheet_native/extconf.rb
|
|
63
|
+
- ext/calcsheet_native/src/csv.rs
|
|
64
|
+
- ext/calcsheet_native/src/gvl.rs
|
|
65
|
+
- ext/calcsheet_native/src/lib.rs
|
|
66
|
+
- ext/calcsheet_native/src/ods.rs
|
|
67
|
+
- ext/calcsheet_native/src/xlsx.rs
|
|
68
|
+
- ext/calcsheet_native/src/xml.rs
|
|
69
|
+
- lib/calcsheet/native.rb
|
|
70
|
+
- lib/calcsheet/native/csv_reader.rb
|
|
71
|
+
- lib/calcsheet/native/ods_reader.rb
|
|
72
|
+
- lib/calcsheet/native/version.rb
|
|
73
|
+
- lib/calcsheet/native/xlsx_reader.rb
|
|
74
|
+
homepage: https://gitlab.opinioni.net/demetra-developers/rubygems/calcsheet-native
|
|
75
|
+
licenses:
|
|
76
|
+
- MIT
|
|
77
|
+
metadata:
|
|
78
|
+
allowed_push_host: https://rubygems.org
|
|
79
|
+
source_code_uri: https://gitlab.opinioni.net/demetra-developers/rubygems/calcsheet-native/-/tree/main
|
|
80
|
+
bug_tracker_uri: https://gitlab.opinioni.net/demetra-developers/rubygems/calcsheet-native/-/issues
|
|
81
|
+
changelog_uri: https://gitlab.opinioni.net/demetra-developers/rubygems/calcsheet-native/-/blob/main/CHANGELOG.md
|
|
82
|
+
rubygems_mfa_required: 'true'
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '3.3'
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 4.0.16
|
|
98
|
+
specification_version: 4
|
|
99
|
+
summary: Native reader backends for calcsheet.
|
|
100
|
+
test_files: []
|