xlsxrb 0.1.4 → 0.1.6
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/.devcontainer/Dockerfile +1 -1
- data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +16 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +96 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +68 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +47 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
- data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/csv/3.3/csv.rbs +3871 -0
- data/.gem_rbs_collection/csv/3.3/manifest.yaml +3 -0
- data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +48 -0
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
- data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
- data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/logger/1.7/formatter.rbs +45 -0
- data/.gem_rbs_collection/logger/1.7/log_device.rbs +100 -0
- data/.gem_rbs_collection/logger/1.7/logger.rbs +796 -0
- data/.gem_rbs_collection/logger/1.7/manifest.yaml +2 -0
- data/.gem_rbs_collection/logger/1.7/period.rbs +17 -0
- data/.gem_rbs_collection/logger/1.7/severity.rbs +34 -0
- data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
- data/.gem_rbs_collection/parser/3.2/parser.rbs +194 -0
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +208 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +903 -0
- data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
- data/CHANGELOG.md +42 -3
- data/README.md +130 -49
- data/Rakefile +23 -2
- data/Steepfile +17 -0
- data/benchmark.rb +324 -348
- data/docs/ARCHITECTURE.md +24 -0
- data/docs/DEVELOPMENT.md +58 -0
- data/docs/QUALITY_ASSURANCE.md +26 -0
- data/docs/visual/VisualGallery.md +893 -938
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/xlsxrb/elements/cell.rb +113 -8
- data/lib/xlsxrb/elements/column.rb +2 -0
- data/lib/xlsxrb/elements/row.rb +52 -4
- data/lib/xlsxrb/elements/types.rb +6 -0
- data/lib/xlsxrb/elements/workbook.rb +29 -0
- data/lib/xlsxrb/elements/worksheet.rb +98 -1
- data/lib/xlsxrb/elements.rb +2 -0
- data/lib/xlsxrb/ooxml/reader.rb +8 -6
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +106 -40
- data/lib/xlsxrb/ooxml/styles_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/utils.rb +2 -0
- data/lib/xlsxrb/ooxml/workbook_parser.rb +6 -1
- data/lib/xlsxrb/ooxml/workbook_writer.rb +15 -4
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +156 -23
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +207 -152
- data/lib/xlsxrb/ooxml/writer.rb +217 -5
- data/lib/xlsxrb/ooxml/xml_builder.rb +18 -10
- data/lib/xlsxrb/ooxml/xml_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/zip_generator.rb +5 -8
- data/lib/xlsxrb/ooxml/zip_reader.rb +179 -57
- data/lib/xlsxrb/ooxml/zip_writer.rb +65 -25
- data/lib/xlsxrb/ooxml.rb +3 -1
- data/lib/xlsxrb/style_builder.rb +284 -36
- data/lib/xlsxrb/version.rb +3 -1
- data/lib/xlsxrb.rb +1761 -275
- data/rbs_collection.lock.yaml +252 -0
- data/rbs_collection.yaml +19 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +30 -0
- data/sig/generated/xlsxrb/elements/column.rbs +30 -0
- data/sig/generated/xlsxrb/elements/row.rbs +32 -0
- data/sig/generated/xlsxrb/elements/types.rbs +74 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +25 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +27 -0
- data/sig/generated/xlsxrb/elements.rbs +8 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
- data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
- data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
- data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml.rbs +23 -0
- data/sig/generated/xlsxrb/style_builder.rbs +250 -0
- data/sig/generated/xlsxrb/version.rbs +5 -0
- data/sig/generated/xlsxrb.rbs +1276 -0
- data/sig/rexml.rbs +4 -0
- metadata +145 -4
- data/measure_memory.rb +0 -42
- data/sig/xlsxrb.rbs +0 -23
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require_relative "xml_parser"
|
|
4
6
|
|
|
5
7
|
module Xlsxrb
|
|
@@ -7,13 +9,20 @@ module Xlsxrb
|
|
|
7
9
|
# SAX-based parser for xl/sharedStrings.xml.
|
|
8
10
|
# Returns an Array of strings (index = SST index).
|
|
9
11
|
class SharedStringsParser
|
|
12
|
+
XML_ENTITIES = { "&" => "&", "<" => "<", ">" => ">", """ => '"', "'" => "'" }.freeze
|
|
13
|
+
|
|
10
14
|
# Parses all shared strings and returns an Array of strings.
|
|
11
|
-
def self.parse(xml_string,
|
|
15
|
+
def self.parse(xml_string, _part_name: "xl/sharedStrings.xml")
|
|
12
16
|
return [] if xml_string.nil? || xml_string.empty?
|
|
13
17
|
|
|
18
|
+
if xml_string.include?("<!DOCTYPE") || xml_string.include?("<!ENTITY")
|
|
19
|
+
listener = Class.new { include REXML::SAX2Listener }.new
|
|
20
|
+
XmlParser.parse(xml_string, listener)
|
|
21
|
+
end
|
|
22
|
+
|
|
14
23
|
strings = []
|
|
15
|
-
|
|
16
|
-
strings <<
|
|
24
|
+
fast_scan(xml_string) do |str|
|
|
25
|
+
strings << str
|
|
17
26
|
end
|
|
18
27
|
strings
|
|
19
28
|
end
|
|
@@ -23,53 +32,110 @@ module Xlsxrb
|
|
|
23
32
|
return enum_for(:each_event, xml_string, part_name: part_name) unless block
|
|
24
33
|
return if xml_string.nil? || xml_string.empty?
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
index = 0
|
|
36
|
+
fast_scan(xml_string) do |str|
|
|
37
|
+
block.call(Event.new(
|
|
38
|
+
type: :sst_item,
|
|
39
|
+
args: [str],
|
|
40
|
+
source: { part: part_name, index: index }
|
|
41
|
+
))
|
|
42
|
+
index += 1
|
|
43
|
+
end
|
|
28
44
|
end
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def initialize(part_name, &block)
|
|
35
|
-
@part_name = part_name
|
|
36
|
-
@block = block
|
|
37
|
-
@in_si = false
|
|
38
|
-
@in_t = false
|
|
39
|
-
@current_text = +""
|
|
40
|
-
@index = 0
|
|
41
|
-
end
|
|
46
|
+
def self.fast_scan(xml_src)
|
|
47
|
+
xml = xml_src.b
|
|
48
|
+
sst_start = xml.index("<sst")
|
|
49
|
+
return unless sst_start
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
sst_open_end = xml.index(">", sst_start)
|
|
52
|
+
return unless sst_open_end
|
|
53
|
+
return if xml.getbyte(sst_open_end - 1) == 47 # self-closing <sst/>
|
|
54
|
+
|
|
55
|
+
sst_end = xml.index("</sst>", sst_open_end)
|
|
56
|
+
sst_end ||= xml.bytesize
|
|
57
|
+
|
|
58
|
+
pos = sst_open_end + 1
|
|
59
|
+
while pos < sst_end
|
|
60
|
+
si_start = xml.index("<si", pos)
|
|
61
|
+
break unless si_start && si_start < sst_end
|
|
62
|
+
|
|
63
|
+
si_open_end = xml.index(">", si_start + 3)
|
|
64
|
+
break unless si_open_end
|
|
65
|
+
|
|
66
|
+
# Self-closing <si/>
|
|
67
|
+
if xml.getbyte(si_open_end - 1) == 47
|
|
68
|
+
yield ""
|
|
69
|
+
pos = si_open_end + 1
|
|
70
|
+
next
|
|
50
71
|
end
|
|
51
|
-
end
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
si_end = xml.index("</si>", si_open_end + 1)
|
|
74
|
+
break unless si_end
|
|
75
|
+
|
|
76
|
+
# Fast path: check for simple single <t>...</t> inside <si>
|
|
77
|
+
first_t = xml.index("<t", si_open_end + 1)
|
|
78
|
+
if first_t && first_t < si_end
|
|
79
|
+
t_open_end = xml.index(">", first_t + 2)
|
|
80
|
+
if t_open_end && t_open_end < si_end
|
|
81
|
+
if xml.getbyte(t_open_end - 1) == 47 # <t/>
|
|
82
|
+
str = ""
|
|
83
|
+
else
|
|
84
|
+
t_end = xml.index("</t>", t_open_end + 1)
|
|
85
|
+
if t_end && t_end <= si_end
|
|
86
|
+
next_t = xml.index("<t", t_end + 4)
|
|
87
|
+
if next_t && next_t < si_end
|
|
88
|
+
# Multiple <t> tags (rich text runs)
|
|
89
|
+
str = extract_multi_t(xml, si_open_end + 1, si_end)
|
|
90
|
+
else
|
|
91
|
+
raw_str = xml.byteslice(t_open_end + 1, t_end - t_open_end - 1).force_encoding("UTF-8")
|
|
92
|
+
str = raw_str.include?("&") ? raw_str.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES) : raw_str
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
str = ""
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
str = ""
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
str = ""
|
|
66
103
|
end
|
|
104
|
+
|
|
105
|
+
yield str.freeze
|
|
106
|
+
pos = si_end + 5
|
|
67
107
|
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private_class_method :fast_scan
|
|
111
|
+
|
|
112
|
+
def self.extract_multi_t(xml, from, to)
|
|
113
|
+
buf = +""
|
|
114
|
+
pos = from
|
|
115
|
+
while pos < to
|
|
116
|
+
t_start = xml.index("<t", pos)
|
|
117
|
+
break unless t_start && t_start < to
|
|
68
118
|
|
|
69
|
-
|
|
70
|
-
|
|
119
|
+
t_open_end = xml.index(">", t_start + 2)
|
|
120
|
+
break unless t_open_end
|
|
121
|
+
|
|
122
|
+
if xml.getbyte(t_open_end - 1) == 47
|
|
123
|
+
pos = t_open_end + 1
|
|
124
|
+
next
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
t_end = xml.index("</t>", t_open_end + 1)
|
|
128
|
+
break unless t_end && t_end <= to
|
|
129
|
+
|
|
130
|
+
raw_chunk = xml.byteslice(t_open_end + 1, t_end - t_open_end - 1).force_encoding("UTF-8")
|
|
131
|
+
raw_chunk = raw_chunk.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES) if raw_chunk.include?("&")
|
|
132
|
+
buf << raw_chunk
|
|
133
|
+
pos = t_end + 4
|
|
71
134
|
end
|
|
135
|
+
buf
|
|
72
136
|
end
|
|
137
|
+
|
|
138
|
+
private_class_method :extract_multi_t
|
|
73
139
|
end
|
|
74
140
|
end
|
|
75
141
|
end
|
data/lib/xlsxrb/ooxml/utils.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require_relative "xml_parser"
|
|
4
6
|
|
|
5
7
|
module Xlsxrb
|
|
@@ -28,8 +30,11 @@ module Xlsxrb
|
|
|
28
30
|
def start_element(_uri, localname, _qname, attrs)
|
|
29
31
|
return unless localname == "sheet"
|
|
30
32
|
|
|
33
|
+
name_attr = attrs["name"]
|
|
34
|
+
name_attr = REXML::Text.unnormalize(name_attr) if name_attr
|
|
35
|
+
|
|
31
36
|
@sheets << {
|
|
32
|
-
name:
|
|
37
|
+
name: name_attr,
|
|
33
38
|
sheet_id: attrs["sheetId"]&.to_i,
|
|
34
39
|
r_id: attrs["r:id"] || attrs["id"] || attrs.find { |k, _| k.end_with?(":id") }&.last
|
|
35
40
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "stringio"
|
|
4
6
|
require_relative "xml_builder"
|
|
5
7
|
require_relative "zip_writer"
|
|
@@ -30,19 +32,19 @@ module Xlsxrb
|
|
|
30
32
|
|
|
31
33
|
def self.write(target, sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
|
|
32
34
|
defined_names: nil, core_properties: nil, app_properties: nil,
|
|
33
|
-
custom_properties: nil, workbook_protection: nil)
|
|
34
|
-
Xlsxrb
|
|
35
|
+
custom_properties: nil, workbook_protection: nil, workbook_properties: nil)
|
|
36
|
+
Xlsxrb.in_span("Ooxml::WorkbookWriter.write") do
|
|
35
37
|
writer = new(sheets: sheets, shared_strings: shared_strings, shared_strings_index: shared_strings_index, styles: styles,
|
|
36
38
|
defined_names: defined_names, core_properties: core_properties,
|
|
37
39
|
app_properties: app_properties, custom_properties: custom_properties,
|
|
38
|
-
workbook_protection: workbook_protection)
|
|
40
|
+
workbook_protection: workbook_protection, workbook_properties: workbook_properties)
|
|
39
41
|
writer.write_to(target)
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def initialize(sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
|
|
44
46
|
defined_names: nil, core_properties: nil, app_properties: nil,
|
|
45
|
-
custom_properties: nil, workbook_protection: nil)
|
|
47
|
+
custom_properties: nil, workbook_protection: nil, workbook_properties: nil)
|
|
46
48
|
@sheets = sheets
|
|
47
49
|
@shared_strings = shared_strings
|
|
48
50
|
@shared_strings_index = shared_strings_index
|
|
@@ -52,6 +54,7 @@ module Xlsxrb
|
|
|
52
54
|
@app_properties = app_properties || {}
|
|
53
55
|
@custom_properties = custom_properties || []
|
|
54
56
|
@workbook_protection = workbook_protection
|
|
57
|
+
@workbook_properties = workbook_properties || { update_links: "never" }
|
|
55
58
|
@drawing_count = 0
|
|
56
59
|
@chart_count = 0
|
|
57
60
|
@comment_count = 0
|
|
@@ -436,6 +439,14 @@ module Xlsxrb
|
|
|
436
439
|
"xmlns:r": DOC_REL
|
|
437
440
|
})
|
|
438
441
|
|
|
442
|
+
# Workbook properties
|
|
443
|
+
if @workbook_properties && !@workbook_properties.empty?
|
|
444
|
+
attrs = {}
|
|
445
|
+
attrs[:date1904] = "1" if @workbook_properties[:date1904]
|
|
446
|
+
attrs[:updateLinks] = @workbook_properties[:update_links] if @workbook_properties[:update_links]
|
|
447
|
+
b.empty_tag("workbookPr", attrs) unless attrs.empty?
|
|
448
|
+
end
|
|
449
|
+
|
|
439
450
|
# Workbook protection
|
|
440
451
|
if @workbook_protection
|
|
441
452
|
wp_attrs = {}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require_relative "xml_parser"
|
|
4
6
|
|
|
5
7
|
module Xlsxrb
|
|
@@ -116,6 +118,13 @@ module Xlsxrb
|
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
# ---- Fast string-scanning event parser (byte-level) ----
|
|
121
|
+
#
|
|
122
|
+
# SECURITY NOTE: This custom XML parsing approach is secure by design against
|
|
123
|
+
# typical XML vulnerabilities:
|
|
124
|
+
# - XXE (XML External Entity Expansion): It does not interpret DTDs or expand
|
|
125
|
+
# arbitrary entities. It purely scans for literal `<row>` and `<c>` tags.
|
|
126
|
+
# - ReDoS: It uses O(1) byte indexing and bounded `String#index` searches
|
|
127
|
+
# rather than unbounded or backtracking regular expressions.
|
|
119
128
|
|
|
120
129
|
def self.fast_scan_events(xml_src, shared_strings, part_name, &block)
|
|
121
130
|
xml = xml_src.b # force ASCII-8BIT for O(1) byte indexing
|
|
@@ -384,8 +393,7 @@ module Xlsxrb
|
|
|
384
393
|
if raw.include?(".")
|
|
385
394
|
raw.to_f
|
|
386
395
|
else
|
|
387
|
-
|
|
388
|
-
int_val.to_s == raw ? int_val : raw.to_f
|
|
396
|
+
raw.to_i
|
|
389
397
|
end
|
|
390
398
|
end
|
|
391
399
|
end
|
|
@@ -394,6 +402,9 @@ module Xlsxrb
|
|
|
394
402
|
|
|
395
403
|
XML_ENTITIES = { "&" => "&", "<" => "<", ">" => ">", """ => '"', "'" => "'" }.freeze
|
|
396
404
|
|
|
405
|
+
# SECURITY NOTE: Decodes only standard predefined XML entities in a single pass.
|
|
406
|
+
# This completely prevents "Billion Laughs" attacks (exponential entity expansion)
|
|
407
|
+
# because it avoids recursive expansion and ignores custom entities entirely.
|
|
397
408
|
def self.decode_xml_entities(str)
|
|
398
409
|
str.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES)
|
|
399
410
|
end
|
|
@@ -470,19 +481,53 @@ module Xlsxrb
|
|
|
470
481
|
next
|
|
471
482
|
end
|
|
472
483
|
|
|
473
|
-
row_tag = xml.byteslice(row_start, tag_end - row_start)
|
|
474
484
|
row_index = 0
|
|
475
|
-
|
|
476
|
-
|
|
485
|
+
has_custom_attrs = false
|
|
486
|
+
ri = row_start + 4
|
|
487
|
+
while ri < tag_end
|
|
488
|
+
rb = xml.getbyte(ri)
|
|
489
|
+
if rb == 114 && xml.getbyte(ri + 1) == 61 && xml.getbyte(ri + 2) == 34 # r="
|
|
490
|
+
ri += 3
|
|
491
|
+
while ri < tag_end
|
|
492
|
+
cb = xml.getbyte(ri)
|
|
493
|
+
break unless cb.between?(48, 57)
|
|
494
|
+
|
|
495
|
+
row_index = (row_index * 10) + (cb - 48)
|
|
496
|
+
ri += 1
|
|
497
|
+
|
|
498
|
+
end
|
|
499
|
+
row_index -= 1
|
|
500
|
+
elsif [104, 99, 111].include?(rb) # 'h', 'c', 'o' for ht, customHeight, hidden, outlineLevel
|
|
501
|
+
has_custom_attrs = true
|
|
502
|
+
ri += 1
|
|
503
|
+
else
|
|
504
|
+
ri += 1
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
attrs = if has_custom_attrs
|
|
509
|
+
row_tag = xml.byteslice(row_start, tag_end - row_start)
|
|
510
|
+
extract_row_attrs(row_tag)
|
|
511
|
+
else
|
|
512
|
+
EMPTY_HASH
|
|
513
|
+
end
|
|
477
514
|
|
|
478
|
-
attrs = extract_row_attrs(row_tag)
|
|
479
515
|
row_end = xml.index("</row>", tag_end + 1)
|
|
480
516
|
break unless row_end
|
|
481
517
|
|
|
482
518
|
row_source = { part: part_name, row: row_index }
|
|
483
519
|
cells = fast_parse_cells_direct(xml, tag_end + 1, row_end, shared_strings, row_source)
|
|
484
520
|
|
|
485
|
-
|
|
521
|
+
row_obj = Elements::Row.new(
|
|
522
|
+
index: row_index,
|
|
523
|
+
cells: cells,
|
|
524
|
+
height: attrs[:height],
|
|
525
|
+
hidden: attrs[:hidden] || false,
|
|
526
|
+
custom_height: attrs[:custom_height] || false,
|
|
527
|
+
outline_level: attrs[:outline_level],
|
|
528
|
+
errors: Elements::EMPTY_ERRORS
|
|
529
|
+
)
|
|
530
|
+
block.call(row_obj)
|
|
486
531
|
|
|
487
532
|
pos = row_end + 6
|
|
488
533
|
end
|
|
@@ -507,18 +552,90 @@ module Xlsxrb
|
|
|
507
552
|
c_tag_end = xml.index(">", c_start + 2)
|
|
508
553
|
break unless c_tag_end
|
|
509
554
|
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
555
|
+
# Zero-allocation attribute scan directly within the tag
|
|
556
|
+
type = nil
|
|
557
|
+
style_index = nil
|
|
558
|
+
col_idx = nil
|
|
559
|
+
row_idx = nil
|
|
560
|
+
|
|
561
|
+
ai = c_start + 2
|
|
562
|
+
while ai < c_tag_end
|
|
563
|
+
b = xml.getbyte(ai)
|
|
564
|
+
if [32, 9, 10, 13].include?(b)
|
|
565
|
+
ai += 1
|
|
566
|
+
next
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
if b == 114 && xml.getbyte(ai + 1) == 61 && xml.getbyte(ai + 2) == 34 # r="
|
|
570
|
+
ai += 3
|
|
571
|
+
col_idx = 0
|
|
572
|
+
while ai < c_tag_end
|
|
573
|
+
cb = xml.getbyte(ai)
|
|
574
|
+
if cb.between?(65, 90)
|
|
575
|
+
col_idx = (col_idx * 26) + (cb - 64)
|
|
576
|
+
ai += 1
|
|
577
|
+
elsif cb.between?(97, 122)
|
|
578
|
+
col_idx = (col_idx * 26) + (cb - 96)
|
|
579
|
+
ai += 1
|
|
580
|
+
else
|
|
581
|
+
break
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
col_idx -= 1
|
|
585
|
+
row_idx = 0
|
|
586
|
+
while ai < c_tag_end
|
|
587
|
+
cb = xml.getbyte(ai)
|
|
588
|
+
break unless cb.between?(48, 57)
|
|
589
|
+
|
|
590
|
+
row_idx = (row_idx * 10) + (cb - 48)
|
|
591
|
+
ai += 1
|
|
592
|
+
|
|
593
|
+
end
|
|
594
|
+
row_idx -= 1
|
|
595
|
+
ai += 1 if xml.getbyte(ai) == 34
|
|
596
|
+
elsif b == 116 && xml.getbyte(ai + 1) == 61 && xml.getbyte(ai + 2) == 34 # t="
|
|
597
|
+
ai += 3
|
|
598
|
+
type_b = xml.getbyte(ai)
|
|
599
|
+
if type_b == 115 && xml.getbyte(ai + 1) == 34 # "s"
|
|
600
|
+
type = "s"
|
|
601
|
+
ai += 2
|
|
602
|
+
elsif type_b == 98 && xml.getbyte(ai + 1) == 34 # "b"
|
|
603
|
+
type = "b"
|
|
604
|
+
ai += 2
|
|
605
|
+
elsif type_b == 101 && xml.getbyte(ai + 1) == 34 # "e"
|
|
606
|
+
type = "e"
|
|
607
|
+
ai += 2
|
|
608
|
+
else
|
|
609
|
+
t_end = xml.index('"', ai)
|
|
610
|
+
type = xml.byteslice(ai, t_end - ai) if t_end
|
|
611
|
+
ai = t_end ? t_end + 1 : c_tag_end
|
|
612
|
+
end
|
|
613
|
+
elsif b == 115 && xml.getbyte(ai + 1) == 61 && xml.getbyte(ai + 2) == 34 # s="
|
|
614
|
+
ai += 3
|
|
615
|
+
style_index = 0
|
|
616
|
+
while ai < c_tag_end
|
|
617
|
+
cb = xml.getbyte(ai)
|
|
618
|
+
break unless cb.between?(48, 57)
|
|
619
|
+
|
|
620
|
+
style_index = (style_index * 10) + (cb - 48)
|
|
621
|
+
ai += 1
|
|
622
|
+
|
|
623
|
+
end
|
|
624
|
+
ai += 1 if xml.getbyte(ai) == 34
|
|
625
|
+
else
|
|
626
|
+
ai += 1
|
|
627
|
+
end
|
|
628
|
+
end
|
|
516
629
|
|
|
517
630
|
# Self-closing <c ... />
|
|
518
631
|
if xml.getbyte(c_tag_end - 1) == 47
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
632
|
+
cells << Elements::Cell.new(
|
|
633
|
+
row_index: row_idx || row_source[:row],
|
|
634
|
+
column_index: col_idx || cells.size,
|
|
635
|
+
value: nil,
|
|
636
|
+
style_index: style_index,
|
|
637
|
+
errors: Elements::EMPTY_ERRORS
|
|
638
|
+
)
|
|
522
639
|
pos = c_tag_end + 1
|
|
523
640
|
next
|
|
524
641
|
end
|
|
@@ -542,8 +659,21 @@ module Xlsxrb
|
|
|
542
659
|
v_val_start = tag_pos + 3
|
|
543
660
|
v_end = xml.index("</v>", v_val_start)
|
|
544
661
|
if v_end
|
|
545
|
-
|
|
546
|
-
|
|
662
|
+
if type == "s"
|
|
663
|
+
# Zero-allocation integer parse for SST index
|
|
664
|
+
s_idx = 0
|
|
665
|
+
v_i = v_val_start
|
|
666
|
+
while v_i < v_end
|
|
667
|
+
s_idx = (s_idx * 10) + (xml.getbyte(v_i) - 48)
|
|
668
|
+
v_i += 1
|
|
669
|
+
end
|
|
670
|
+
value = shared_strings[s_idx] || ""
|
|
671
|
+
elsif type == "b"
|
|
672
|
+
value = xml.getbyte(v_val_start) == 49 # '1'
|
|
673
|
+
else
|
|
674
|
+
raw_value = xml.byteslice(v_val_start, v_end - v_val_start)
|
|
675
|
+
value = resolve_fast_value(raw_value, type, shared_strings)
|
|
676
|
+
end
|
|
547
677
|
cpos = v_end + 4
|
|
548
678
|
else
|
|
549
679
|
cpos = tag_pos + 3
|
|
@@ -591,11 +721,14 @@ module Xlsxrb
|
|
|
591
721
|
end
|
|
592
722
|
|
|
593
723
|
val_to_use = inline_str || value
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
724
|
+
cells << Elements::Cell.new(
|
|
725
|
+
row_index: row_idx || row_source[:row],
|
|
726
|
+
column_index: col_idx || cells.size,
|
|
727
|
+
value: val_to_use,
|
|
728
|
+
formula: formula,
|
|
729
|
+
style_index: style_index,
|
|
730
|
+
errors: Elements::EMPTY_ERRORS
|
|
731
|
+
)
|
|
599
732
|
|
|
600
733
|
pos = c_end + 4
|
|
601
734
|
end
|