xlsxrb 0.1.5 → 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/.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/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 +18 -0
- data/README.md +39 -32
- data/Rakefile +2 -1
- data/Steepfile +0 -2
- data/benchmark.rb +376 -0
- data/docs/ARCHITECTURE.md +12 -5
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/xlsxrb/elements/cell.rb +39 -7
- data/lib/xlsxrb/elements/row.rb +17 -4
- data/lib/xlsxrb/elements/types.rb +4 -0
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +104 -40
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +144 -23
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +160 -145
- data/lib/xlsxrb/ooxml/xml_builder.rb +16 -10
- data/lib/xlsxrb/ooxml/zip_reader.rb +104 -18
- data/lib/xlsxrb/ooxml/zip_writer.rb +63 -25
- data/lib/xlsxrb/style_builder.rb +158 -30
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb.rb +443 -236
- data/rbs_collection.lock.yaml +28 -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/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/style_builder.rbs +212 -50
- data/sig/generated/xlsxrb.rbs +1276 -0
- data/sig/rexml.rbs +4 -0
- metadata +76 -1
|
@@ -13,11 +13,13 @@ module Xlsxrb
|
|
|
13
13
|
class ZipReader
|
|
14
14
|
LOCAL_HEADER_SIG = "PK\x03\x04".b
|
|
15
15
|
MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024 # 500MB per file limit
|
|
16
|
+
attr_accessor :max_uncompressed_size
|
|
16
17
|
|
|
17
18
|
# Opens a ZIP from a file path or IO and yields the reader.
|
|
18
|
-
|
|
19
|
+
#: (untyped source, ?max_uncompressed_size: Integer) ?{ (ZipReader) -> untyped } -> (ZipReader | untyped)
|
|
20
|
+
def self.open(source, max_uncompressed_size: MAX_UNCOMPRESSED_SIZE)
|
|
19
21
|
io = source.is_a?(String) ? File.open(source, "rb") : source
|
|
20
|
-
reader = new(io)
|
|
22
|
+
reader = new(io, max_uncompressed_size: max_uncompressed_size)
|
|
21
23
|
if block_given?
|
|
22
24
|
begin
|
|
23
25
|
yield reader
|
|
@@ -29,8 +31,10 @@ module Xlsxrb
|
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
#: (untyped io, ?max_uncompressed_size: Integer) -> void
|
|
35
|
+
def initialize(io, max_uncompressed_size: MAX_UNCOMPRESSED_SIZE)
|
|
33
36
|
@io = io
|
|
37
|
+
@max_uncompressed_size = max_uncompressed_size
|
|
34
38
|
@entries = nil
|
|
35
39
|
end
|
|
36
40
|
|
|
@@ -60,8 +64,6 @@ module Xlsxrb
|
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
def parse_entries
|
|
63
|
-
result = {}
|
|
64
|
-
|
|
65
67
|
io = if @io.is_a?(StringIO) || (@io.respond_to?(:read) && @io.respond_to?(:seek) && @io.respond_to?(:pos))
|
|
66
68
|
@io
|
|
67
69
|
elsif @io.respond_to?(:each) || @io.is_a?(Enumerator)
|
|
@@ -79,9 +81,21 @@ module Xlsxrb
|
|
|
79
81
|
first_sig = io.read(4)
|
|
80
82
|
raise ArgumentError, "Invalid magic number: Expected a valid ZIP/XLSX file format (PK\\x03\\x04)" unless first_sig == LOCAL_HEADER_SIG
|
|
81
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
|
+
|
|
82
94
|
io.seek(-4, IO::SEEK_CUR) if io.respond_to?(:seek)
|
|
83
95
|
io = StringIO.new(first_sig + io.read.b) unless io.respond_to?(:seek)
|
|
84
96
|
|
|
97
|
+
result = {}
|
|
98
|
+
|
|
85
99
|
loop do
|
|
86
100
|
sig = io.read(4)
|
|
87
101
|
break unless sig == LOCAL_HEADER_SIG
|
|
@@ -101,11 +115,11 @@ module Xlsxrb
|
|
|
101
115
|
has_data_descriptor = gp_flag.anybits?(0x08)
|
|
102
116
|
|
|
103
117
|
if has_data_descriptor && compressed_size.zero?
|
|
104
|
-
|
|
118
|
+
entry_data, = find_data_descriptor_stream(io, method)
|
|
105
119
|
else
|
|
106
120
|
raw = io.read(compressed_size)
|
|
121
|
+
entry_data = decompress(raw, method)
|
|
107
122
|
end
|
|
108
|
-
entry_data = decompress(raw, method)
|
|
109
123
|
result[entry_name] = entry_data unless entry_name.end_with?("/")
|
|
110
124
|
end
|
|
111
125
|
|
|
@@ -117,36 +131,107 @@ module Xlsxrb
|
|
|
117
131
|
result
|
|
118
132
|
end
|
|
119
133
|
|
|
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
|
+
|
|
120
201
|
def find_data_descriptor_stream(io, method)
|
|
121
202
|
if method == 8
|
|
122
203
|
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
123
204
|
result = +""
|
|
124
|
-
consumed = 0
|
|
125
205
|
chunk_size = 4096
|
|
126
206
|
begin
|
|
127
|
-
while (chunk = io.read(chunk_size))
|
|
207
|
+
while !inflater.finished? && (chunk = io.read(chunk_size))
|
|
128
208
|
break if chunk.empty?
|
|
129
209
|
|
|
130
210
|
inflated = inflater.inflate(chunk)
|
|
131
|
-
|
|
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
|
|
132
213
|
|
|
133
214
|
result << inflated
|
|
134
|
-
consumed += chunk.bytesize
|
|
135
215
|
end
|
|
136
216
|
rescue Zlib::BufError, Zlib::DataError
|
|
137
217
|
# Inflation ended
|
|
138
218
|
ensure
|
|
139
|
-
|
|
219
|
+
remaining_bytes = inflater.avail_in
|
|
140
220
|
inflater.close
|
|
141
221
|
end
|
|
142
222
|
|
|
143
|
-
io.seek(-
|
|
223
|
+
io.seek(-remaining_bytes, IO::SEEK_CUR) if io.respond_to?(:seek) && remaining_bytes.positive?
|
|
144
224
|
|
|
145
225
|
desc_sig = io.read(4)
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
148
233
|
|
|
149
|
-
[result,
|
|
234
|
+
[result, 0]
|
|
150
235
|
else
|
|
151
236
|
[io.read(0), 0]
|
|
152
237
|
end
|
|
@@ -168,19 +253,20 @@ module Xlsxrb
|
|
|
168
253
|
chunk_size = 32_768
|
|
169
254
|
offset = 0
|
|
170
255
|
raw_len = raw.bytesize
|
|
256
|
+
limit = @max_uncompressed_size || MAX_UNCOMPRESSED_SIZE
|
|
171
257
|
|
|
172
258
|
begin
|
|
173
259
|
while offset < raw_len
|
|
174
260
|
chunk = raw.byteslice(offset, chunk_size)
|
|
175
261
|
inflated = inflater.inflate(chunk)
|
|
176
|
-
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{
|
|
262
|
+
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{limit} bytes" if result.bytesize + inflated.bytesize > limit
|
|
177
263
|
|
|
178
264
|
result << inflated
|
|
179
265
|
offset += chunk_size
|
|
180
266
|
end
|
|
181
267
|
# Finish inflation
|
|
182
268
|
inflated = inflater.finish
|
|
183
|
-
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{
|
|
269
|
+
raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{limit} bytes" if result.bytesize + inflated.bytesize > limit
|
|
184
270
|
|
|
185
271
|
result << inflated
|
|
186
272
|
ensure
|
|
@@ -28,6 +28,8 @@ module Xlsxrb
|
|
|
28
28
|
@io = io
|
|
29
29
|
@entries = []
|
|
30
30
|
@closed = false
|
|
31
|
+
@bytes_written = 0
|
|
32
|
+
@seekable = determine_seekable
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# Add a file entry with string content.
|
|
@@ -51,17 +53,22 @@ module Xlsxrb
|
|
|
51
53
|
def start_entry(path)
|
|
52
54
|
raise "ZipWriter is closed" if @closed
|
|
53
55
|
|
|
56
|
+
entry_offset = @bytes_written
|
|
57
|
+
gp_flag = @seekable ? 0 : 0x0008
|
|
58
|
+
|
|
54
59
|
@current_entry = {
|
|
55
60
|
path: path,
|
|
56
|
-
offset:
|
|
61
|
+
offset: entry_offset,
|
|
57
62
|
deflater: Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS),
|
|
58
63
|
crc: Zlib.crc32,
|
|
59
64
|
uncompressed_size: 0,
|
|
60
|
-
compressed_size: 0
|
|
65
|
+
compressed_size: 0,
|
|
66
|
+
gp_flag: gp_flag,
|
|
67
|
+
seekable: @seekable
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
# Write
|
|
64
|
-
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)
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
def write_data(str)
|
|
@@ -74,7 +81,7 @@ module Xlsxrb
|
|
|
74
81
|
@current_entry[:uncompressed_size] += bytes.bytesize
|
|
75
82
|
compressed = @current_entry[:deflater].deflate(bytes, Zlib::SYNC_FLUSH)
|
|
76
83
|
@current_entry[:compressed_size] += compressed.bytesize
|
|
77
|
-
|
|
84
|
+
write_bytes(compressed)
|
|
78
85
|
end
|
|
79
86
|
|
|
80
87
|
def finish_entry
|
|
@@ -86,17 +93,21 @@ module Xlsxrb
|
|
|
86
93
|
# Flush remaining deflate data
|
|
87
94
|
remaining = entry[:deflater].finish
|
|
88
95
|
entry[:compressed_size] += remaining.bytesize
|
|
89
|
-
|
|
96
|
+
write_bytes(remaining)
|
|
90
97
|
entry[:deflater].close
|
|
91
98
|
|
|
92
99
|
final_crc = entry[:crc] & 0xFFFFFFFF
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if entry[:seekable]
|
|
102
|
+
# Patch the local header if seekable
|
|
103
|
+
current_pos = @io.is_a?(StringIO) ? @io.pos : @io.tell
|
|
97
104
|
@io.seek(entry[:offset])
|
|
98
|
-
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)
|
|
99
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)
|
|
100
111
|
end
|
|
101
112
|
|
|
102
113
|
@entries << {
|
|
@@ -104,7 +115,8 @@ module Xlsxrb
|
|
|
104
115
|
crc32: final_crc,
|
|
105
116
|
compressed_size: entry[:compressed_size],
|
|
106
117
|
uncompressed_size: entry[:uncompressed_size],
|
|
107
|
-
offset: entry[:offset]
|
|
118
|
+
offset: entry[:offset],
|
|
119
|
+
gp_flag: entry[:gp_flag]
|
|
108
120
|
}
|
|
109
121
|
end
|
|
110
122
|
|
|
@@ -114,28 +126,47 @@ module Xlsxrb
|
|
|
114
126
|
finish_entry if @current_entry
|
|
115
127
|
|
|
116
128
|
@closed = true
|
|
117
|
-
cd_offset = @
|
|
129
|
+
cd_offset = @bytes_written
|
|
118
130
|
cd_size = write_central_directory
|
|
119
131
|
write_end_of_central_directory(cd_offset, cd_size)
|
|
120
132
|
end
|
|
121
133
|
|
|
122
134
|
private
|
|
123
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
|
+
|
|
124
155
|
def write_raw_entry(path, content_bytes)
|
|
125
156
|
crc = Zlib.crc32(content_bytes) & 0xFFFFFFFF
|
|
126
157
|
compressed = deflate(content_bytes)
|
|
158
|
+
offset = @bytes_written
|
|
127
159
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
write_local_header(path, crc, compressed.bytesize, content_bytes.bytesize)
|
|
131
|
-
@io.write(compressed)
|
|
160
|
+
write_local_header(path, crc, compressed.bytesize, content_bytes.bytesize, gp_flag: 0)
|
|
161
|
+
write_bytes(compressed)
|
|
132
162
|
|
|
133
163
|
@entries << {
|
|
134
164
|
path: path,
|
|
135
165
|
crc32: crc,
|
|
136
166
|
compressed_size: compressed.bytesize,
|
|
137
167
|
uncompressed_size: content_bytes.bytesize,
|
|
138
|
-
offset: offset
|
|
168
|
+
offset: offset,
|
|
169
|
+
gp_flag: 0
|
|
139
170
|
}
|
|
140
171
|
end
|
|
141
172
|
|
|
@@ -148,12 +179,12 @@ module Xlsxrb
|
|
|
148
179
|
end
|
|
149
180
|
end
|
|
150
181
|
|
|
151
|
-
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)
|
|
152
183
|
name_bytes = path.encode("UTF-8").b
|
|
153
184
|
header = [
|
|
154
185
|
0x04034B50, # local file header signature
|
|
155
186
|
20, # version needed (2.0)
|
|
156
|
-
|
|
187
|
+
gp_flag, # general purpose bit flag (0x0008 if data descriptor follows)
|
|
157
188
|
8, # compression method (deflate)
|
|
158
189
|
0, # last mod file time
|
|
159
190
|
33, # last mod file date
|
|
@@ -163,8 +194,14 @@ module Xlsxrb
|
|
|
163
194
|
name_bytes.bytesize,
|
|
164
195
|
0 # extra field length
|
|
165
196
|
].pack("VvvvvvVVVvv")
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
168
205
|
end
|
|
169
206
|
|
|
170
207
|
def write_central_directory
|
|
@@ -175,7 +212,7 @@ module Xlsxrb
|
|
|
175
212
|
0x02014B50, # central directory file header signature
|
|
176
213
|
20, # version made by
|
|
177
214
|
20, # version needed
|
|
178
|
-
0,
|
|
215
|
+
entry[:gp_flag] || 0, # general purpose bit flag
|
|
179
216
|
8, # compression method
|
|
180
217
|
0, # last mod file time
|
|
181
218
|
33, # last mod file date
|
|
@@ -190,8 +227,9 @@ module Xlsxrb
|
|
|
190
227
|
0, # external file attributes
|
|
191
228
|
entry[:offset] # relative offset of local header
|
|
192
229
|
].pack("VvvvvvvVVVvvvvvVV")
|
|
193
|
-
|
|
194
|
-
|
|
230
|
+
|
|
231
|
+
write_bytes(header)
|
|
232
|
+
write_bytes(name_bytes)
|
|
195
233
|
size += header.bytesize + name_bytes.bytesize
|
|
196
234
|
end
|
|
197
235
|
size
|
|
@@ -208,7 +246,7 @@ module Xlsxrb
|
|
|
208
246
|
cd_offset, # offset of central directory
|
|
209
247
|
0 # comment length
|
|
210
248
|
].pack("VvvvvVVv")
|
|
211
|
-
|
|
249
|
+
write_bytes(eocd)
|
|
212
250
|
end
|
|
213
251
|
end
|
|
214
252
|
end
|