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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
  4. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
  5. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
  6. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
  7. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
  8. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
  9. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
  10. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
  11. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
  12. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
  13. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
  14. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
  15. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
  16. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
  17. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
  18. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
  19. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
  20. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
  21. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
  22. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
  23. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
  24. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
  25. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
  26. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
  27. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
  28. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
  29. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
  30. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
  31. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
  32. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
  33. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
  34. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
  35. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
  36. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
  37. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
  38. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
  39. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
  40. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
  41. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
  42. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
  43. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
  44. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
  45. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
  46. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
  47. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
  48. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
  49. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
  50. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
  51. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
  52. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
  53. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
  54. data/CHANGELOG.md +18 -0
  55. data/README.md +39 -32
  56. data/Rakefile +2 -1
  57. data/Steepfile +0 -2
  58. data/benchmark.rb +376 -0
  59. data/docs/ARCHITECTURE.md +12 -5
  60. data/docs/wasm/ruby.wasm +0 -0
  61. data/lib/xlsxrb/elements/cell.rb +39 -7
  62. data/lib/xlsxrb/elements/row.rb +17 -4
  63. data/lib/xlsxrb/elements/types.rb +4 -0
  64. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +104 -40
  65. data/lib/xlsxrb/ooxml/worksheet_parser.rb +144 -23
  66. data/lib/xlsxrb/ooxml/worksheet_writer.rb +160 -145
  67. data/lib/xlsxrb/ooxml/xml_builder.rb +16 -10
  68. data/lib/xlsxrb/ooxml/zip_reader.rb +104 -18
  69. data/lib/xlsxrb/ooxml/zip_writer.rb +63 -25
  70. data/lib/xlsxrb/style_builder.rb +158 -30
  71. data/lib/xlsxrb/version.rb +1 -1
  72. data/lib/xlsxrb.rb +443 -236
  73. data/rbs_collection.lock.yaml +28 -0
  74. data/sig/generated/xlsxrb/elements/cell.rbs +30 -0
  75. data/sig/generated/xlsxrb/elements/column.rbs +30 -0
  76. data/sig/generated/xlsxrb/elements/row.rbs +32 -0
  77. data/sig/generated/xlsxrb/elements/types.rbs +74 -0
  78. data/sig/generated/xlsxrb/elements/workbook.rbs +25 -0
  79. data/sig/generated/xlsxrb/elements/worksheet.rbs +27 -0
  80. data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
  81. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
  82. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
  83. data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
  84. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
  85. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
  86. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
  87. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
  88. data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
  89. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
  90. data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
  91. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
  92. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
  93. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
  94. data/sig/generated/xlsxrb/style_builder.rbs +212 -50
  95. data/sig/generated/xlsxrb.rbs +1276 -0
  96. data/sig/rexml.rbs +4 -0
  97. 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
- def self.open(source)
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
- def initialize(io)
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
- raw, = find_data_descriptor_stream(io, method)
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
- raise ArgumentError, "ZIP bomb detected: Uncompressed size exceeds #{MAX_UNCOMPRESSED_SIZE} bytes" if result.bytesize + inflated.bytesize > MAX_UNCOMPRESSED_SIZE
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
- consumed -= inflater.avail_in
219
+ remaining_bytes = inflater.avail_in
140
220
  inflater.close
141
221
  end
142
222
 
143
- io.seek(-inflater.avail_in, IO::SEEK_CUR) if io.respond_to?(: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
- io.seek(-4, IO::SEEK_CUR) unless desc_sig == [0x50, 0x4B, 0x07, 0x08].pack("C4")
147
- io.read(12)
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, consumed]
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 #{MAX_UNCOMPRESSED_SIZE} bytes" if result.bytesize + inflated.bytesize > MAX_UNCOMPRESSED_SIZE
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 #{MAX_UNCOMPRESSED_SIZE} bytes" if result.bytesize + inflated.bytesize > MAX_UNCOMPRESSED_SIZE
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: @io.is_a?(StringIO) ? @io.pos : @io.tell,
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 a placeholder local header (will be patched later if IO supports seek)
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
- @io.write(compressed)
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
- @io.write(remaining)
96
+ write_bytes(remaining)
90
97
  entry[:deflater].close
91
98
 
92
99
  final_crc = entry[:crc] & 0xFFFFFFFF
93
100
 
94
- # Patch the local header if seekable
95
- current_pos = @io.is_a?(StringIO) ? @io.pos : @io.tell
96
- if @io.respond_to?(:seek)
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 = @io.is_a?(StringIO) ? @io.pos : @io.tell
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
- offset = @io.is_a?(StringIO) ? @io.pos : @io.tell
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
- 0, # general purpose bit flag
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
- @io.write(header)
167
- @io.write(name_bytes)
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, # general purpose bit flag
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
- @io.write(header)
194
- @io.write(name_bytes)
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
- @io.write(eocd)
249
+ write_bytes(eocd)
212
250
  end
213
251
  end
214
252
  end