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
|
module Xlsxrb
|
|
4
6
|
module Ooxml
|
|
5
7
|
# Streams well-formed XML to a writable IO without building a DOM.
|
|
@@ -25,8 +27,10 @@ module Xlsxrb
|
|
|
25
27
|
self
|
|
26
28
|
end
|
|
27
29
|
|
|
30
|
+
EMPTY_HASH = {}.freeze
|
|
31
|
+
|
|
28
32
|
# Opens a tag, yields for children, then closes the tag.
|
|
29
|
-
def tag(name, attrs =
|
|
33
|
+
def tag(name, attrs = EMPTY_HASH, &block)
|
|
30
34
|
if block
|
|
31
35
|
open_tag(name, attrs)
|
|
32
36
|
yield self
|
|
@@ -37,22 +41,26 @@ module Xlsxrb
|
|
|
37
41
|
self
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
def open_tag(name, attrs =
|
|
41
|
-
@io
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
def open_tag(name, attrs = EMPTY_HASH)
|
|
45
|
+
@io.write("<")
|
|
46
|
+
@io.write(name)
|
|
47
|
+
write_attrs(attrs) unless attrs.empty?
|
|
48
|
+
@io.write(">")
|
|
44
49
|
self
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
def close_tag(name)
|
|
48
|
-
@io
|
|
53
|
+
@io.write("</")
|
|
54
|
+
@io.write(name)
|
|
55
|
+
@io.write(">")
|
|
49
56
|
self
|
|
50
57
|
end
|
|
51
58
|
|
|
52
|
-
def empty_tag(name, attrs =
|
|
53
|
-
@io
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
def empty_tag(name, attrs = EMPTY_HASH)
|
|
60
|
+
@io.write("<")
|
|
61
|
+
@io.write(name)
|
|
62
|
+
write_attrs(attrs) unless attrs.empty?
|
|
63
|
+
@io.write("/>")
|
|
56
64
|
self
|
|
57
65
|
end
|
|
58
66
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "zlib"
|
|
4
6
|
|
|
5
7
|
module Xlsxrb
|
|
@@ -142,20 +144,15 @@ module Xlsxrb
|
|
|
142
144
|
end
|
|
143
145
|
|
|
144
146
|
def le16(value)
|
|
145
|
-
[
|
|
147
|
+
[value].pack("v").bytes
|
|
146
148
|
end
|
|
147
149
|
|
|
148
150
|
def le32(value)
|
|
149
|
-
[
|
|
151
|
+
[value].pack("V").bytes
|
|
150
152
|
end
|
|
151
153
|
|
|
152
154
|
def calculate_central_dir_size
|
|
153
|
-
|
|
154
|
-
@entries.each do |entry|
|
|
155
|
-
filename = entry[:path]
|
|
156
|
-
size += 46 + filename.bytesize
|
|
157
|
-
end
|
|
158
|
-
size
|
|
155
|
+
@entries.sum { |entry| 46 + entry[:path].bytesize }
|
|
159
156
|
end
|
|
160
157
|
end
|
|
161
158
|
end
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "zlib"
|
|
4
6
|
require "stringio"
|
|
7
|
+
require "tempfile"
|
|
5
8
|
|
|
6
9
|
module Xlsxrb
|
|
7
10
|
module Ooxml
|
|
8
11
|
# Reads ZIP archives using only stdlib (zlib).
|
|
9
12
|
# Scans local file headers sequentially — works with non-seekable IO.
|
|
10
13
|
class ZipReader
|
|
11
|
-
LOCAL_HEADER_SIG =
|
|
14
|
+
LOCAL_HEADER_SIG = "PK\x03\x04".b
|
|
15
|
+
MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 # 500MB per file limit
|
|
16
|
+
attr_accessor :max_uncompressed_size
|
|
12
17
|
|
|
13
18
|
# Opens a ZIP from a file path or IO and yields the reader.
|
|
14
|
-
|
|
19
|
+
#: (untyped source, ?max_uncompressed_size: Integer) ?{ (ZipReader) -> untyped } -> (ZipReader | untyped)
|
|
20
|
+
def self.open(source, max_uncompressed_size: MAX_UNCOMPRESSED_SIZE)
|
|
15
21
|
io = source.is_a?(String) ? File.open(source, "rb") : source
|
|
16
|
-
reader = new(io)
|
|
22
|
+
reader = new(io, max_uncompressed_size: max_uncompressed_size)
|
|
17
23
|
if block_given?
|
|
18
24
|
begin
|
|
19
25
|
yield reader
|
|
@@ -25,8 +31,10 @@ module Xlsxrb
|
|
|
25
31
|
end
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
#: (untyped io, ?max_uncompressed_size: Integer) -> void
|
|
35
|
+
def initialize(io, max_uncompressed_size: MAX_UNCOMPRESSED_SIZE)
|
|
29
36
|
@io = io
|
|
37
|
+
@max_uncompressed_size = max_uncompressed_size
|
|
30
38
|
@entries = nil
|
|
31
39
|
end
|
|
32
40
|
|
|
@@ -56,86 +64,176 @@ module Xlsxrb
|
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
def parse_entries
|
|
67
|
+
io = if @io.is_a?(StringIO) || (@io.respond_to?(:read) && @io.respond_to?(:seek) && @io.respond_to?(:pos))
|
|
68
|
+
@io
|
|
69
|
+
elsif @io.respond_to?(:each) || @io.is_a?(Enumerator)
|
|
70
|
+
tf = Tempfile.new("xlsxrb_zip")
|
|
71
|
+
tf.binmode
|
|
72
|
+
@io.each { |chunk| tf.write(chunk) }
|
|
73
|
+
tf.rewind
|
|
74
|
+
tf
|
|
75
|
+
else
|
|
76
|
+
StringIO.new(@io.read.b)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
io.binmode if io.respond_to?(:binmode)
|
|
80
|
+
|
|
81
|
+
first_sig = io.read(4)
|
|
82
|
+
raise ArgumentError, "Invalid magic number: Expected a valid ZIP/XLSX file format (PK\\x03\\x04)" unless first_sig == LOCAL_HEADER_SIG
|
|
83
|
+
|
|
84
|
+
# Try parsing via Central Directory first (standard and handles Data Descriptors perfectly)
|
|
85
|
+
cd_result = parse_from_central_directory(io)
|
|
86
|
+
if cd_result && !cd_result.empty?
|
|
87
|
+
if io.is_a?(Tempfile)
|
|
88
|
+
io.close
|
|
89
|
+
io.unlink
|
|
90
|
+
end
|
|
91
|
+
return cd_result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
io.seek(-4, IO::SEEK_CUR) if io.respond_to?(:seek)
|
|
95
|
+
io = StringIO.new(first_sig + io.read.b) unless io.respond_to?(:seek)
|
|
96
|
+
|
|
59
97
|
result = {}
|
|
60
|
-
data = @io.is_a?(StringIO) ? @io.string : @io.read
|
|
61
|
-
data = data.b
|
|
62
|
-
pos = 0
|
|
63
98
|
|
|
64
|
-
|
|
65
|
-
sig =
|
|
99
|
+
loop do
|
|
100
|
+
sig = io.read(4)
|
|
66
101
|
break unless sig == LOCAL_HEADER_SIG
|
|
67
102
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
break if pos + 30 > data.bytesize
|
|
103
|
+
header = io.read(26)
|
|
104
|
+
break unless header && header.bytesize == 26
|
|
71
105
|
|
|
72
|
-
gp_flag
|
|
73
|
-
method
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
name_len = data[pos + 26, 2].unpack1("v")
|
|
78
|
-
extra_len = data[pos + 28, 2].unpack1("v")
|
|
106
|
+
gp_flag = header[2, 2].unpack1("v")
|
|
107
|
+
method = header[4, 2].unpack1("v")
|
|
108
|
+
compressed_size = header[14, 4].unpack1("V")
|
|
109
|
+
name_len = header[22, 2].unpack1("v")
|
|
110
|
+
extra_len = header[24, 2].unpack1("v")
|
|
79
111
|
|
|
80
|
-
entry_name =
|
|
81
|
-
|
|
112
|
+
entry_name = io.read(name_len).force_encoding("UTF-8")
|
|
113
|
+
io.read(extra_len) # skip extra
|
|
82
114
|
|
|
83
|
-
# Data descriptor present if bit 3 of gp_flag is set
|
|
84
115
|
has_data_descriptor = gp_flag.anybits?(0x08)
|
|
85
116
|
|
|
86
117
|
if has_data_descriptor && compressed_size.zero?
|
|
87
|
-
|
|
88
|
-
raw, comp_sz, = find_data_descriptor(data, file_data_offset, method)
|
|
89
|
-
entry_data = decompress(raw, method)
|
|
90
|
-
result[entry_name] = entry_data unless entry_name.end_with?("/")
|
|
91
|
-
# Skip past data + descriptor (12 or 16 bytes)
|
|
92
|
-
desc_offset = file_data_offset + comp_sz
|
|
93
|
-
# Check for optional signature
|
|
94
|
-
pos = if desc_offset + 4 <= data.bytesize && data[desc_offset, 4] == [0x50, 0x4B, 0x07, 0x08].pack("C4")
|
|
95
|
-
desc_offset + 16
|
|
96
|
-
else
|
|
97
|
-
desc_offset + 12
|
|
98
|
-
end
|
|
118
|
+
entry_data, = find_data_descriptor_stream(io, method)
|
|
99
119
|
else
|
|
100
|
-
raw =
|
|
120
|
+
raw = io.read(compressed_size)
|
|
101
121
|
entry_data = decompress(raw, method)
|
|
102
|
-
result[entry_name] = entry_data unless entry_name.end_with?("/")
|
|
103
|
-
pos = file_data_offset + compressed_size
|
|
104
122
|
end
|
|
123
|
+
result[entry_name] = entry_data unless entry_name.end_with?("/")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if io.is_a?(Tempfile)
|
|
127
|
+
io.close
|
|
128
|
+
io.unlink
|
|
105
129
|
end
|
|
106
130
|
|
|
107
131
|
result
|
|
108
132
|
end
|
|
109
133
|
|
|
110
|
-
def
|
|
111
|
-
|
|
134
|
+
def parse_from_central_directory(io)
|
|
135
|
+
return nil unless io.respond_to?(:seek) && io.respond_to?(:pos)
|
|
136
|
+
|
|
137
|
+
io.seek(0, IO::SEEK_END)
|
|
138
|
+
file_size = io.pos
|
|
139
|
+
return nil if file_size < 22
|
|
140
|
+
|
|
141
|
+
max_search = [file_size, 65_557].min
|
|
142
|
+
search_offset = file_size - max_search
|
|
143
|
+
io.seek(search_offset, IO::SEEK_SET)
|
|
144
|
+
search_buf = io.read(max_search)
|
|
145
|
+
return nil unless search_buf
|
|
146
|
+
|
|
147
|
+
eocd_index = search_buf.rindex("PK\x05\x06")
|
|
148
|
+
return nil unless eocd_index
|
|
149
|
+
|
|
150
|
+
eocd_pos = search_offset + eocd_index
|
|
151
|
+
io.seek(eocd_pos + 4, IO::SEEK_SET)
|
|
152
|
+
eocd_data = io.read(18)
|
|
153
|
+
return nil unless eocd_data && eocd_data.bytesize == 18
|
|
154
|
+
|
|
155
|
+
_disk_num, _cd_disk, _disk_entries, total_entries, _cd_size, cd_offset, _comment_len = eocd_data.unpack("vvvvVVv")
|
|
156
|
+
return nil if cd_offset > file_size
|
|
157
|
+
|
|
158
|
+
io.seek(cd_offset, IO::SEEK_SET)
|
|
159
|
+
result = {}
|
|
160
|
+
|
|
161
|
+
total_entries.times do
|
|
162
|
+
sig = io.read(4)
|
|
163
|
+
break unless sig == "PK\x01\x02"
|
|
164
|
+
|
|
165
|
+
cd_header = io.read(42)
|
|
166
|
+
break unless cd_header && cd_header.bytesize == 42
|
|
167
|
+
|
|
168
|
+
_v_made, _v_need, _gp, method, _time, _date, _crc, csize, _usize, nlen, elen, clen, _dnum, _iattr, _eattr, offset = cd_header.unpack("vvvvvvVVVvvvvvVV")
|
|
169
|
+
entry_name = io.read(nlen).force_encoding("UTF-8")
|
|
170
|
+
io.read(elen + clen)
|
|
171
|
+
|
|
172
|
+
next if entry_name.end_with?("/")
|
|
173
|
+
|
|
174
|
+
current_cd_pos = io.pos
|
|
175
|
+
|
|
176
|
+
io.seek(offset, IO::SEEK_SET)
|
|
177
|
+
local_sig = io.read(4)
|
|
178
|
+
next unless local_sig == LOCAL_HEADER_SIG
|
|
179
|
+
|
|
180
|
+
local_header = io.read(26)
|
|
181
|
+
next unless local_header && local_header.bytesize == 26
|
|
182
|
+
|
|
183
|
+
local_nlen = local_header[22, 2].unpack1("v")
|
|
184
|
+
local_elen = local_header[24, 2].unpack1("v")
|
|
185
|
+
io.seek(offset + 30 + local_nlen + local_elen, IO::SEEK_SET)
|
|
186
|
+
|
|
187
|
+
raw = io.read(csize)
|
|
188
|
+
entry_data = decompress(raw, method)
|
|
189
|
+
result[entry_name] = entry_data
|
|
190
|
+
|
|
191
|
+
io.seek(current_cd_pos, IO::SEEK_SET)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
result
|
|
195
|
+
rescue ArgumentError => e
|
|
196
|
+
raise e
|
|
197
|
+
rescue StandardError
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def find_data_descriptor_stream(io, method)
|
|
112
202
|
if method == 8
|
|
113
203
|
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
114
204
|
result = +""
|
|
115
|
-
consumed = 0
|
|
116
205
|
chunk_size = 4096
|
|
117
|
-
pos = offset
|
|
118
206
|
begin
|
|
119
|
-
while
|
|
120
|
-
|
|
121
|
-
|
|
207
|
+
while !inflater.finished? && (chunk = io.read(chunk_size))
|
|
208
|
+
break if chunk.empty?
|
|
209
|
+
|
|
210
|
+
inflated = inflater.inflate(chunk)
|
|
211
|
+
limit = @max_uncompressed_size || MAX_UNCOMPRESSED_SIZE
|
|
212
|
+
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{limit} bytes" if result.bytesize + inflated.bytesize > limit
|
|
122
213
|
|
|
123
|
-
result <<
|
|
124
|
-
consumed += chunk.bytesize
|
|
125
|
-
pos += chunk.bytesize
|
|
214
|
+
result << inflated
|
|
126
215
|
end
|
|
127
216
|
rescue Zlib::BufError, Zlib::DataError
|
|
128
|
-
# Inflation ended
|
|
217
|
+
# Inflation ended
|
|
129
218
|
ensure
|
|
130
|
-
|
|
131
|
-
consumed -= inflater.avail_in
|
|
219
|
+
remaining_bytes = inflater.avail_in
|
|
132
220
|
inflater.close
|
|
133
221
|
end
|
|
134
|
-
|
|
222
|
+
|
|
223
|
+
io.seek(-remaining_bytes, IO::SEEK_CUR) if io.respond_to?(:seek) && remaining_bytes.positive?
|
|
224
|
+
|
|
225
|
+
desc_sig = io.read(4)
|
|
226
|
+
if desc_sig == [0x50, 0x4B, 0x07, 0x08].pack("C4")
|
|
227
|
+
io.read(12)
|
|
228
|
+
elsif desc_sig == [0x50, 0x4B, 0x03, 0x04].pack("C4") || desc_sig == [0x50, 0x4B, 0x01, 0x02].pack("C4")
|
|
229
|
+
io.seek(-4, IO::SEEK_CUR) if io.respond_to?(:seek)
|
|
230
|
+
elsif desc_sig
|
|
231
|
+
io.read(8)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
[result, 0]
|
|
135
235
|
else
|
|
136
|
-
|
|
137
|
-
# This is a fallback; stored + data descriptor is rare
|
|
138
|
-
[data[offset, 0], 0, 0]
|
|
236
|
+
[io.read(0), 0]
|
|
139
237
|
end
|
|
140
238
|
end
|
|
141
239
|
|
|
@@ -143,15 +241,39 @@ module Xlsxrb
|
|
|
143
241
|
return raw&.dup&.force_encoding("UTF-8") || "" if method.zero? # stored
|
|
144
242
|
|
|
145
243
|
# Deflated
|
|
146
|
-
|
|
244
|
+
safe_inflate(raw || "", -Zlib::MAX_WBITS)
|
|
147
245
|
rescue Zlib::DataError
|
|
148
246
|
# Try with raw deflate (no header)
|
|
149
|
-
|
|
247
|
+
safe_inflate(raw || "", -Zlib::MAX_WBITS)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def safe_inflate(raw, wbits)
|
|
251
|
+
inflater = Zlib::Inflate.new(wbits)
|
|
252
|
+
result = +""
|
|
253
|
+
chunk_size = 32_768
|
|
254
|
+
offset = 0
|
|
255
|
+
raw_len = raw.bytesize
|
|
256
|
+
limit = @max_uncompressed_size || MAX_UNCOMPRESSED_SIZE
|
|
257
|
+
|
|
150
258
|
begin
|
|
151
|
-
|
|
259
|
+
while offset < raw_len
|
|
260
|
+
chunk = raw.byteslice(offset, chunk_size)
|
|
261
|
+
inflated = inflater.inflate(chunk)
|
|
262
|
+
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{limit} bytes" if result.bytesize + inflated.bytesize > limit
|
|
263
|
+
|
|
264
|
+
result << inflated
|
|
265
|
+
offset += chunk_size
|
|
266
|
+
end
|
|
267
|
+
# Finish inflation
|
|
268
|
+
inflated = inflater.finish
|
|
269
|
+
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{limit} bytes" if result.bytesize + inflated.bytesize > limit
|
|
270
|
+
|
|
271
|
+
result << inflated
|
|
152
272
|
ensure
|
|
153
273
|
inflater.close
|
|
154
274
|
end
|
|
275
|
+
|
|
276
|
+
result.force_encoding("UTF-8")
|
|
155
277
|
end
|
|
156
278
|
end
|
|
157
279
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "zlib"
|
|
4
6
|
|
|
5
7
|
module Xlsxrb
|
|
@@ -26,6 +28,8 @@ module Xlsxrb
|
|
|
26
28
|
@io = io
|
|
27
29
|
@entries = []
|
|
28
30
|
@closed = false
|
|
31
|
+
@bytes_written = 0
|
|
32
|
+
@seekable = determine_seekable
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
# Add a file entry with string content.
|
|
@@ -49,17 +53,22 @@ module Xlsxrb
|
|
|
49
53
|
def start_entry(path)
|
|
50
54
|
raise "ZipWriter is closed" if @closed
|
|
51
55
|
|
|
56
|
+
entry_offset = @bytes_written
|
|
57
|
+
gp_flag = @seekable ? 0 : 0x0008
|
|
58
|
+
|
|
52
59
|
@current_entry = {
|
|
53
60
|
path: path,
|
|
54
|
-
offset:
|
|
61
|
+
offset: entry_offset,
|
|
55
62
|
deflater: Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS),
|
|
56
63
|
crc: Zlib.crc32,
|
|
57
64
|
uncompressed_size: 0,
|
|
58
|
-
compressed_size: 0
|
|
65
|
+
compressed_size: 0,
|
|
66
|
+
gp_flag: gp_flag,
|
|
67
|
+
seekable: @seekable
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
# Write
|
|
62
|
-
write_local_header(path, 0, 0, 0)
|
|
70
|
+
# Write local header (if unseekable, sizes/crc are 0 and bit 3 is set)
|
|
71
|
+
write_local_header(path, 0, 0, 0, gp_flag: gp_flag)
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
def write_data(str)
|
|
@@ -72,7 +81,7 @@ module Xlsxrb
|
|
|
72
81
|
@current_entry[:uncompressed_size] += bytes.bytesize
|
|
73
82
|
compressed = @current_entry[:deflater].deflate(bytes, Zlib::SYNC_FLUSH)
|
|
74
83
|
@current_entry[:compressed_size] += compressed.bytesize
|
|
75
|
-
|
|
84
|
+
write_bytes(compressed)
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
def finish_entry
|
|
@@ -84,17 +93,21 @@ module Xlsxrb
|
|
|
84
93
|
# Flush remaining deflate data
|
|
85
94
|
remaining = entry[:deflater].finish
|
|
86
95
|
entry[:compressed_size] += remaining.bytesize
|
|
87
|
-
|
|
96
|
+
write_bytes(remaining)
|
|
88
97
|
entry[:deflater].close
|
|
89
98
|
|
|
90
99
|
final_crc = entry[:crc] & 0xFFFFFFFF
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
if entry[:seekable]
|
|
102
|
+
# Patch the local header if seekable
|
|
103
|
+
current_pos = @io.is_a?(StringIO) ? @io.pos : @io.tell
|
|
95
104
|
@io.seek(entry[:offset])
|
|
96
|
-
write_local_header(entry[:path], final_crc, entry[:compressed_size], entry[:uncompressed_size])
|
|
105
|
+
write_local_header(entry[:path], final_crc, entry[:compressed_size], entry[:uncompressed_size], gp_flag: 0, count_bytes: false)
|
|
97
106
|
@io.seek(current_pos)
|
|
107
|
+
else
|
|
108
|
+
# Write Data Descriptor (signature 0x08074b50 + crc32 + comp_size + uncomp_size)
|
|
109
|
+
descriptor = [0x08074B50, final_crc, entry[:compressed_size], entry[:uncompressed_size]].pack("VVVV")
|
|
110
|
+
write_bytes(descriptor)
|
|
98
111
|
end
|
|
99
112
|
|
|
100
113
|
@entries << {
|
|
@@ -102,7 +115,8 @@ module Xlsxrb
|
|
|
102
115
|
crc32: final_crc,
|
|
103
116
|
compressed_size: entry[:compressed_size],
|
|
104
117
|
uncompressed_size: entry[:uncompressed_size],
|
|
105
|
-
offset: entry[:offset]
|
|
118
|
+
offset: entry[:offset],
|
|
119
|
+
gp_flag: entry[:gp_flag]
|
|
106
120
|
}
|
|
107
121
|
end
|
|
108
122
|
|
|
@@ -112,28 +126,47 @@ module Xlsxrb
|
|
|
112
126
|
finish_entry if @current_entry
|
|
113
127
|
|
|
114
128
|
@closed = true
|
|
115
|
-
cd_offset = @
|
|
129
|
+
cd_offset = @bytes_written
|
|
116
130
|
cd_size = write_central_directory
|
|
117
131
|
write_end_of_central_directory(cd_offset, cd_size)
|
|
118
132
|
end
|
|
119
133
|
|
|
120
134
|
private
|
|
121
135
|
|
|
136
|
+
def determine_seekable
|
|
137
|
+
return true if @io.is_a?(StringIO)
|
|
138
|
+
return false unless @io.respond_to?(:seek) && @io.respond_to?(:tell)
|
|
139
|
+
|
|
140
|
+
begin
|
|
141
|
+
@io.tell
|
|
142
|
+
true
|
|
143
|
+
rescue SystemCallError, IOError
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def write_bytes(data)
|
|
149
|
+
return if data.nil? || data.empty?
|
|
150
|
+
|
|
151
|
+
@io.write(data)
|
|
152
|
+
@bytes_written += data.bytesize
|
|
153
|
+
end
|
|
154
|
+
|
|
122
155
|
def write_raw_entry(path, content_bytes)
|
|
123
156
|
crc = Zlib.crc32(content_bytes) & 0xFFFFFFFF
|
|
124
157
|
compressed = deflate(content_bytes)
|
|
158
|
+
offset = @bytes_written
|
|
125
159
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
write_local_header(path, crc, compressed.bytesize, content_bytes.bytesize)
|
|
129
|
-
@io.write(compressed)
|
|
160
|
+
write_local_header(path, crc, compressed.bytesize, content_bytes.bytesize, gp_flag: 0)
|
|
161
|
+
write_bytes(compressed)
|
|
130
162
|
|
|
131
163
|
@entries << {
|
|
132
164
|
path: path,
|
|
133
165
|
crc32: crc,
|
|
134
166
|
compressed_size: compressed.bytesize,
|
|
135
167
|
uncompressed_size: content_bytes.bytesize,
|
|
136
|
-
offset: offset
|
|
168
|
+
offset: offset,
|
|
169
|
+
gp_flag: 0
|
|
137
170
|
}
|
|
138
171
|
end
|
|
139
172
|
|
|
@@ -146,12 +179,12 @@ module Xlsxrb
|
|
|
146
179
|
end
|
|
147
180
|
end
|
|
148
181
|
|
|
149
|
-
def write_local_header(path, crc, compressed_size, uncompressed_size)
|
|
182
|
+
def write_local_header(path, crc, compressed_size, uncompressed_size, gp_flag: 0, count_bytes: true)
|
|
150
183
|
name_bytes = path.encode("UTF-8").b
|
|
151
184
|
header = [
|
|
152
185
|
0x04034B50, # local file header signature
|
|
153
186
|
20, # version needed (2.0)
|
|
154
|
-
|
|
187
|
+
gp_flag, # general purpose bit flag (0x0008 if data descriptor follows)
|
|
155
188
|
8, # compression method (deflate)
|
|
156
189
|
0, # last mod file time
|
|
157
190
|
33, # last mod file date
|
|
@@ -161,8 +194,14 @@ module Xlsxrb
|
|
|
161
194
|
name_bytes.bytesize,
|
|
162
195
|
0 # extra field length
|
|
163
196
|
].pack("VvvvvvVVVvv")
|
|
164
|
-
|
|
165
|
-
|
|
197
|
+
|
|
198
|
+
if count_bytes
|
|
199
|
+
write_bytes(header)
|
|
200
|
+
write_bytes(name_bytes)
|
|
201
|
+
else
|
|
202
|
+
@io.write(header)
|
|
203
|
+
@io.write(name_bytes)
|
|
204
|
+
end
|
|
166
205
|
end
|
|
167
206
|
|
|
168
207
|
def write_central_directory
|
|
@@ -173,7 +212,7 @@ module Xlsxrb
|
|
|
173
212
|
0x02014B50, # central directory file header signature
|
|
174
213
|
20, # version made by
|
|
175
214
|
20, # version needed
|
|
176
|
-
0,
|
|
215
|
+
entry[:gp_flag] || 0, # general purpose bit flag
|
|
177
216
|
8, # compression method
|
|
178
217
|
0, # last mod file time
|
|
179
218
|
33, # last mod file date
|
|
@@ -188,8 +227,9 @@ module Xlsxrb
|
|
|
188
227
|
0, # external file attributes
|
|
189
228
|
entry[:offset] # relative offset of local header
|
|
190
229
|
].pack("VvvvvvvVVVvvvvvVV")
|
|
191
|
-
|
|
192
|
-
|
|
230
|
+
|
|
231
|
+
write_bytes(header)
|
|
232
|
+
write_bytes(name_bytes)
|
|
193
233
|
size += header.bytesize + name_bytes.bytesize
|
|
194
234
|
end
|
|
195
235
|
size
|
|
@@ -206,7 +246,7 @@ module Xlsxrb
|
|
|
206
246
|
cd_offset, # offset of central directory
|
|
207
247
|
0 # comment length
|
|
208
248
|
].pack("VvvvvVVv")
|
|
209
|
-
|
|
249
|
+
write_bytes(eocd)
|
|
210
250
|
end
|
|
211
251
|
end
|
|
212
252
|
end
|
data/lib/xlsxrb/ooxml.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require_relative "ooxml/utils"
|
|
4
6
|
require_relative "ooxml/zip_reader"
|
|
5
7
|
require_relative "ooxml/zip_writer"
|
|
@@ -17,6 +19,6 @@ module Xlsxrb
|
|
|
17
19
|
# Handles ZIP extraction, SAX XML parsing, and XML generation
|
|
18
20
|
# in strict accordance with ECMA-376.
|
|
19
21
|
module Ooxml
|
|
20
|
-
Event =
|
|
22
|
+
Event = Data.define(:type, :args, :source)
|
|
21
23
|
end
|
|
22
24
|
end
|