zip_kit 6.4.1 → 6.4.2

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.
@@ -43,11 +43,64 @@ class ZipKit::ZipWriter
43
43
  [VERSION_MADE_BY, os_type].pack("CC").freeze
44
44
  end
45
45
 
46
- C_UINT4 = "V" # Encode a 4-byte unsigned little-endian uint
47
- C_UINT2 = "v" # Encode a 2-byte unsigned little-endian uint
48
- C_UINT8 = "Q<" # Encode an 8-byte unsigned little-endian uint
49
- C_CHAR = "C" # For bit-encoded strings
50
- C_INT4 = "l<" # Encode a 4-byte signed little-endian int
46
+ # Collects the values of a ZIP record and packs them all with a single `Array#pack`.
47
+ # Packing value-by-value allocates an Array and a String per value, and the ZIP headers have lots of values.
48
+ # All integers are little-endian, as the ZIP format wants them.
49
+ class PackBuffer
50
+ def initialize
51
+ @values = []
52
+ @packspec = +""
53
+ end
54
+
55
+ # @param value[Integer] a 1-byte unsigned int
56
+ # @return [self]
57
+ def uint1(value)
58
+ append(value, "C")
59
+ end
60
+
61
+ # @param value[Integer] a 2-byte unsigned int
62
+ # @return [self]
63
+ def uint2(value)
64
+ append(value, "v")
65
+ end
66
+
67
+ # @param value[Integer] a 4-byte unsigned int
68
+ # @return [self]
69
+ def uint4(value)
70
+ append(value, "V")
71
+ end
72
+
73
+ # @param value[Integer] an 8-byte unsigned int
74
+ # @return [self]
75
+ def uint8(value)
76
+ append(value, "Q<")
77
+ end
78
+
79
+ # @param value[Integer] a 4-byte signed int
80
+ # @return [self]
81
+ def int4(value)
82
+ append(value, "l<")
83
+ end
84
+
85
+ # @param value[String] a String whose bytes get appended as-is, regardless of its encoding
86
+ # @return [self]
87
+ def bytes(value)
88
+ append(value, "a*")
89
+ end
90
+
91
+ # @return [String] the packed values in binary encoding
92
+ def b
93
+ @values.pack(@packspec)
94
+ end
95
+
96
+ private
97
+
98
+ def append(value, packspec)
99
+ @values << value
100
+ @packspec << packspec
101
+ self
102
+ end
103
+ end
51
104
 
52
105
  private_constant :FOUR_BYTE_MAX_UINT,
53
106
  :TWO_BYTE_MAX_UINT,
@@ -57,9 +110,7 @@ class ZipKit::ZipWriter
57
110
  :FILE_TYPE_FILE,
58
111
  :FILE_TYPE_DIRECTORY,
59
112
  :MADE_BY_SIGNATURE,
60
- :C_UINT4,
61
- :C_UINT2,
62
- :C_UINT8,
113
+ :PackBuffer,
63
114
  :ZIP_KIT_COMMENT
64
115
 
65
116
  # Writes the local file header, that precedes the actual file _data_.
@@ -81,61 +132,43 @@ class ZipKit::ZipWriter
81
132
  def write_local_file_header(io:, filename:, compressed_size:, uncompressed_size:, crc32:, gp_flags:, mtime:, storage_mode:, extended_timestamp: true)
82
133
  requires_zip64 = compressed_size > FOUR_BYTE_MAX_UINT || uncompressed_size > FOUR_BYTE_MAX_UINT
83
134
 
135
+ # Interesting tidbit:
136
+ # https://social.technet.microsoft.com/Forums/windows/en-US/6a60399f-2879-4859-b7ab-6ddd08a70948
137
+ # TL;DR of it is: Windows 7 Explorer _will_ open Zip64 entries. However, it desires to have the
138
+ # Zip64 extra field as _the first_ extra field.
139
+ extra_fields = "".b
140
+ extra_fields << zip_64_extra_for_local_file_header(compressed_size: compressed_size, uncompressed_size: uncompressed_size) if requires_zip64
141
+ extra_fields << timestamp_extra_for_local_file_header(mtime) if extended_timestamp
142
+
143
+ buf = PackBuffer.new
84
144
  # local file header signature 4 bytes (0x04034b50)
85
- io << [0x04034b50].pack(C_UINT4)
145
+ buf.uint4(0x04034b50)
86
146
  # version needed to extract 2 bytes
87
- io << if requires_zip64
88
- [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_UINT2)
89
- else
90
- [VERSION_NEEDED_TO_EXTRACT].pack(C_UINT2)
91
- end
92
-
147
+ buf.uint2(requires_zip64 ? VERSION_NEEDED_TO_EXTRACT_ZIP64 : VERSION_NEEDED_TO_EXTRACT)
93
148
  # general purpose bit flag 2 bytes
94
- io << [gp_flags].pack(C_UINT2)
149
+ buf.uint2(gp_flags)
95
150
  # compression method 2 bytes
96
- io << [storage_mode].pack(C_UINT2)
151
+ buf.uint2(storage_mode)
97
152
  # last mod file time 2 bytes
98
- io << [to_binary_dos_time(mtime)].pack(C_UINT2)
153
+ buf.uint2(to_binary_dos_time(mtime))
99
154
  # last mod file date 2 bytes
100
- io << [to_binary_dos_date(mtime)].pack(C_UINT2)
155
+ buf.uint2(to_binary_dos_date(mtime))
101
156
  # crc-32 4 bytes
102
- io << [crc32].pack(C_UINT4)
103
-
104
- if requires_zip64
105
- # compressed size 4 bytes
106
- io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4)
107
- # uncompressed size 4 bytes
108
- io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4)
109
- else
110
- # compressed size 4 bytes
111
- io << [compressed_size].pack(C_UINT4)
112
- # uncompressed size 4 bytes
113
- io << [uncompressed_size].pack(C_UINT4)
114
- end
115
-
157
+ buf.uint4(crc32)
158
+ # compressed size 4 bytes
159
+ buf.uint4(requires_zip64 ? FOUR_BYTE_MAX_UINT : compressed_size)
160
+ # uncompressed size 4 bytes
161
+ buf.uint4(requires_zip64 ? FOUR_BYTE_MAX_UINT : uncompressed_size)
116
162
  # Filename should not be longer than 0xFFFF otherwise this wont fit here
117
- # file name length 2 bytes
118
- io << [filename.bytesize].pack(C_UINT2)
119
-
120
- extra_fields = StringIO.new
121
-
122
- # Interesting tidbit:
123
- # https://social.technet.microsoft.com/Forums/windows/en-US/6a60399f-2879-4859-b7ab-6ddd08a70948
124
- # TL;DR of it is: Windows 7 Explorer _will_ open Zip64 entries. However, it desires to have the
125
- # Zip64 extra field as _the first_ extra field.
126
- if requires_zip64
127
- extra_fields << zip_64_extra_for_local_file_header(compressed_size: compressed_size, uncompressed_size: uncompressed_size)
128
- end
129
- extra_fields << timestamp_extra_for_local_file_header(mtime) if extended_timestamp
130
-
163
+ # file name length 2 bytes
164
+ buf.uint2(filename.bytesize)
131
165
  # extra field length 2 bytes
132
- io << [extra_fields.size].pack(C_UINT2)
133
-
166
+ buf.uint2(extra_fields.bytesize)
134
167
  # file name (variable size)
135
- io << filename
136
-
168
+ buf.bytes(filename)
137
169
  # Contents of the extra fields (variable size)
138
- io << extra_fields.string
170
+ buf.bytes(extra_fields)
171
+ io << buf.b
139
172
  end
140
173
 
141
174
  # Writes the file header for the central directory, for a particular file in the archive. When writing out this data,
@@ -168,94 +201,70 @@ class ZipKit::ZipWriter
168
201
  add_zip64 = (local_file_header_location > FOUR_BYTE_MAX_UINT) ||
169
202
  (compressed_size > FOUR_BYTE_MAX_UINT) || (uncompressed_size > FOUR_BYTE_MAX_UINT)
170
203
 
171
- # central file header signature 4 bytes (0x02014b50)
172
- io << [0x02014b50].pack(C_UINT4)
173
- # version made by 2 bytes
174
- io << MADE_BY_SIGNATURE
204
+ extra_fields = "".b
205
+ if add_zip64
206
+ extra_fields << zip_64_extra_for_central_directory_file_header(local_file_header_location: local_file_header_location,
207
+ compressed_size: compressed_size,
208
+ uncompressed_size: uncompressed_size)
209
+ end
210
+ extra_fields << timestamp_extra_for_central_directory_entry(mtime) if extended_timestamp
175
211
 
176
- # version needed to extract 2 bytes
177
- io << if add_zip64
178
- [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_UINT2)
212
+ # Because the add_empty_directory method will create a directory with a trailing "/",
213
+ # this check can be used to assign proper permissions to the created directory.
214
+ external_attrs = if filename.end_with?("/")
215
+ unix_permissions ||= DEFAULT_DIRECTORY_UNIX_PERMISSIONS
216
+ generate_external_attrs(unix_permissions, FILE_TYPE_DIRECTORY)
179
217
  else
180
- [VERSION_NEEDED_TO_EXTRACT].pack(C_UINT2)
218
+ unix_permissions ||= DEFAULT_FILE_UNIX_PERMISSIONS
219
+ generate_external_attrs(unix_permissions, FILE_TYPE_FILE)
181
220
  end
182
221
 
222
+ buf = PackBuffer.new
223
+ # central file header signature 4 bytes (0x02014b50)
224
+ buf.uint4(0x02014b50)
225
+ # version made by 2 bytes
226
+ buf.bytes(MADE_BY_SIGNATURE)
227
+ # version needed to extract 2 bytes
228
+ buf.uint2(add_zip64 ? VERSION_NEEDED_TO_EXTRACT_ZIP64 : VERSION_NEEDED_TO_EXTRACT)
183
229
  # general purpose bit flag 2 bytes
184
- io << [gp_flags].pack(C_UINT2)
230
+ buf.uint2(gp_flags)
185
231
  # compression method 2 bytes
186
- io << [storage_mode].pack(C_UINT2)
232
+ buf.uint2(storage_mode)
187
233
  # last mod file time 2 bytes
188
- io << [to_binary_dos_time(mtime)].pack(C_UINT2)
234
+ buf.uint2(to_binary_dos_time(mtime))
189
235
  # last mod file date 2 bytes
190
- io << [to_binary_dos_date(mtime)].pack(C_UINT2)
236
+ buf.uint2(to_binary_dos_date(mtime))
191
237
  # crc-32 4 bytes
192
- io << [crc32].pack(C_UINT4)
193
-
194
- # compressed size 4 bytes
195
- # uncompressed size 4 bytes
196
- if add_zip64
197
- io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4)
198
- io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4)
199
- else
200
- io << [compressed_size].pack(C_UINT4)
201
- io << [uncompressed_size].pack(C_UINT4)
202
- end
203
-
238
+ buf.uint4(crc32)
239
+ # compressed size 4 bytes
240
+ buf.uint4(add_zip64 ? FOUR_BYTE_MAX_UINT : compressed_size)
241
+ # uncompressed size 4 bytes
242
+ buf.uint4(add_zip64 ? FOUR_BYTE_MAX_UINT : uncompressed_size)
204
243
  # Filename should not be longer than 0xFFFF otherwise this wont fit here
205
244
  # file name length 2 bytes
206
- io << [filename.bytesize].pack(C_UINT2)
207
-
208
- extra_fields = StringIO.new
209
- if add_zip64
210
- extra_fields << zip_64_extra_for_central_directory_file_header(local_file_header_location: local_file_header_location,
211
- compressed_size: compressed_size,
212
- uncompressed_size: uncompressed_size)
213
- end
214
- extra_fields << timestamp_extra_for_central_directory_entry(mtime) if extended_timestamp
215
-
245
+ buf.uint2(filename.bytesize)
216
246
  # extra field length 2 bytes
217
- io << [extra_fields.size].pack(C_UINT2)
247
+ buf.uint2(extra_fields.bytesize)
218
248
  # file comment length 2 bytes
219
- io << [0].pack(C_UINT2)
220
-
249
+ buf.uint2(0)
221
250
  # For The Unarchiver < 3.11.1 this field has to be set to the overflow value if zip64 is used
222
251
  # because otherwise it does not properly advance the pointer when reading the Zip64 extra field
223
252
  # https://bitbucket.org/WAHa_06x36/theunarchiver/pull-requests/2/bug-fix-for-zip64-extra-field-parser/diff
224
253
  # disk number start 2 bytes
225
- io << if add_zip64
226
- [TWO_BYTE_MAX_UINT].pack(C_UINT2)
227
- else
228
- [0].pack(C_UINT2)
229
- end
254
+ buf.uint2(add_zip64 ? TWO_BYTE_MAX_UINT : 0)
230
255
  # internal file attributes 2 bytes
231
- io << [0].pack(C_UINT2)
232
-
233
- # Because the add_empty_directory method will create a directory with a trailing "/",
234
- # this check can be used to assign proper permissions to the created directory.
235
- external_attrs = if filename.end_with?("/")
236
- unix_permissions ||= DEFAULT_DIRECTORY_UNIX_PERMISSIONS
237
- generate_external_attrs(unix_permissions, FILE_TYPE_DIRECTORY)
238
- else
239
- unix_permissions ||= DEFAULT_FILE_UNIX_PERMISSIONS
240
- generate_external_attrs(unix_permissions, FILE_TYPE_FILE)
241
- end
242
-
256
+ buf.uint2(0)
243
257
  # external file attributes 4 bytes
244
- io << [external_attrs].pack(C_UINT4)
245
-
258
+ buf.uint4(external_attrs)
246
259
  # relative offset of local header 4 bytes
247
- io << if add_zip64
248
- [FOUR_BYTE_MAX_UINT].pack(C_UINT4)
249
- else
250
- [local_file_header_location].pack(C_UINT4)
251
- end
252
-
260
+ buf.uint4(add_zip64 ? FOUR_BYTE_MAX_UINT : local_file_header_location)
253
261
  # file name (variable size)
254
- io << filename
262
+ buf.bytes(filename)
255
263
  # extra field (variable size)
256
- io << extra_fields.string
264
+ buf.bytes(extra_fields)
257
265
  # file comment (variable size)
258
266
  # (empty)
267
+ io << buf.b
259
268
  end
260
269
 
261
270
  # Writes the data descriptor following the file data for a file whose local file header
@@ -268,25 +277,30 @@ class ZipKit::ZipWriter
268
277
  # @param uncompressed_size[Integer] The size of the file once extracted
269
278
  # @return [void]
270
279
  def write_data_descriptor(io:, compressed_size:, uncompressed_size:, crc32:)
271
- # Although not originally assigned a signature, the value
272
- # 0x08074b50 has commonly been adopted as a signature value
273
- # for the data descriptor record.
274
- io << [0x08074b50].pack(C_UINT4)
275
-
276
- # crc-32 4 bytes
277
- io << [crc32].pack(C_UINT4)
278
-
279
280
  # If one of the sizes is above 0xFFFFFFF use ZIP64 lengths (8 bytes) instead. A good unarchiver
280
281
  # will decide to unpack it as such if it finds the Zip64 extra for the file in the central directory.
281
282
  # So also use the opportune moment to switch the entry to Zip64 if needed.
282
283
  # We switch if either of the sizes requires ZIP64, so that both values are encoded similarly.
283
284
  requires_zip64 = compressed_size > FOUR_BYTE_MAX_UINT || uncompressed_size > FOUR_BYTE_MAX_UINT
284
- pack_spec = requires_zip64 ? C_UINT8 : C_UINT4
285
-
286
- # compressed size 4 bytes, or 8 bytes for ZIP64
287
- io << [compressed_size].pack(pack_spec)
288
- # uncompressed size 4 bytes, or 8 bytes for ZIP64
289
- io << [uncompressed_size].pack(pack_spec)
285
+ buf = PackBuffer.new
286
+ # Although not originally assigned a signature, the value
287
+ # 0x08074b50 has commonly been adopted as a signature value
288
+ # for the data descriptor record.
289
+ buf.uint4(0x08074b50)
290
+ # crc-32 4 bytes
291
+ buf.uint4(crc32)
292
+ if requires_zip64
293
+ # compressed size 8 bytes
294
+ buf.uint8(compressed_size)
295
+ # uncompressed size 8 bytes
296
+ buf.uint8(uncompressed_size)
297
+ else
298
+ # compressed size 4 bytes
299
+ buf.uint4(compressed_size)
300
+ # uncompressed size 4 bytes
301
+ buf.uint4(uncompressed_size)
302
+ end
303
+ io << buf.b
290
304
  end
291
305
 
292
306
  # Writes the "end of central directory record" (including the Zip6 salient bits if necessary)
@@ -305,91 +319,81 @@ class ZipKit::ZipWriter
305
319
  zip64_eocdr_offset > FOUR_BYTE_MAX_UINT ||
306
320
  num_files_in_archive > TWO_BYTE_MAX_UINT
307
321
 
322
+ buf = PackBuffer.new
323
+
308
324
  # Then, if zip64 is used
309
325
  if zip64_required
310
326
  # [zip64 end of central directory record]
311
327
  # zip64 end of central dir signature 4 bytes (0x06064b50)
312
- io << [0x06064b50].pack(C_UINT4)
313
-
328
+ buf.uint4(0x06064b50)
314
329
  # size of zip64 end of central
315
330
  # directory record 8 bytes
316
331
  # (this is ex. the 12 bytes of the signature and the size value itself).
317
332
  # Without the extensible data sector (which we are not using)
318
333
  # it is always 44 bytes.
319
- io << [44].pack(C_UINT8)
320
-
334
+ buf.uint8(44)
321
335
  # version made by 2 bytes
322
- io << MADE_BY_SIGNATURE
336
+ buf.bytes(MADE_BY_SIGNATURE)
323
337
  # version needed to extract 2 bytes
324
- io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_UINT2)
338
+ buf.uint2(VERSION_NEEDED_TO_EXTRACT_ZIP64)
325
339
  # number of this disk 4 bytes
326
- io << [0].pack(C_UINT4)
340
+ buf.uint4(0)
327
341
  # number of the disk with the start of the central directory 4 bytes
328
- io << [0].pack(C_UINT4)
342
+ buf.uint4(0)
329
343
  # total number of entries in the
330
344
  # central directory on this disk 8 bytes
331
- io << [num_files_in_archive].pack(C_UINT8)
345
+ buf.uint8(num_files_in_archive)
332
346
  # total number of entries in the
333
347
  # central directory 8 bytes
334
- io << [num_files_in_archive].pack(C_UINT8)
348
+ buf.uint8(num_files_in_archive)
335
349
  # size of the central directory 8 bytes
336
- io << [central_directory_size].pack(C_UINT8)
350
+ buf.uint8(central_directory_size)
337
351
  # offset of start of central directory with respect to
338
352
  # the starting disk number 8 bytes
339
- io << [start_of_central_directory_location].pack(C_UINT8)
353
+ buf.uint8(start_of_central_directory_location)
340
354
  # zip64 extensible data sector (variable size)
341
355
  # (blank for us)
342
356
 
343
357
  # zip64 end of central dir locator
344
358
  # signature 4 bytes (0x07064b50)
345
- io << [0x07064b50].pack(C_UINT4)
359
+ buf.uint4(0x07064b50)
346
360
  # number of the disk with the start of the zip64 end of
347
361
  # central directory 4 bytes
348
- io << [0].pack(C_UINT4)
362
+ buf.uint4(0)
349
363
  # relative offset of the zip64
350
364
  # end of central directory record 8 bytes
351
365
  # (note: "relative" is actually "from the start of the file")
352
- io << [zip64_eocdr_offset].pack(C_UINT8)
366
+ buf.uint8(zip64_eocdr_offset)
353
367
  # total number of disks 4 bytes
354
- io << [1].pack(C_UINT4)
368
+ buf.uint4(1)
355
369
  end
356
370
 
357
371
  # Then the end of central directory record:
358
372
  # end of central dir signature 4 bytes (0x06054b50)
359
- io << [0x06054b50].pack(C_UINT4)
373
+ buf.uint4(0x06054b50)
360
374
  # number of this disk 2 bytes
361
- io << [0].pack(C_UINT2)
375
+ buf.uint2(0)
362
376
  # number of the disk with the
363
377
  # start of the central directory 2 bytes
364
- io << [0].pack(C_UINT2)
365
-
378
+ buf.uint2(0)
366
379
  # total number of entries in the
367
380
  # central directory on this disk 2 bytes
381
+ # (the number of entries will be read from the zip64 part of the central directory if zip64 is used)
382
+ buf.uint2(zip64_required ? TWO_BYTE_MAX_UINT : num_files_in_archive)
368
383
  # total number of entries in
369
384
  # the central directory 2 bytes
370
- if zip64_required # the number of entries will be read from the zip64 part of the central directory
371
- io << [TWO_BYTE_MAX_UINT].pack(C_UINT2)
372
- io << [TWO_BYTE_MAX_UINT].pack(C_UINT2)
373
- else
374
- io << [num_files_in_archive].pack(C_UINT2)
375
- io << [num_files_in_archive].pack(C_UINT2)
376
- end
377
-
385
+ buf.uint2(zip64_required ? TWO_BYTE_MAX_UINT : num_files_in_archive)
378
386
  # size of the central directory 4 bytes
387
+ buf.uint4(zip64_required ? FOUR_BYTE_MAX_UINT : central_directory_size)
379
388
  # offset of start of central
380
389
  # directory with respect to
381
390
  # the starting disk number 4 bytes
382
- if zip64_required
383
- io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4)
384
- io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4)
385
- else
386
- io << [central_directory_size].pack(C_UINT4)
387
- io << [start_of_central_directory_location].pack(C_UINT4)
388
- end
391
+ buf.uint4(zip64_required ? FOUR_BYTE_MAX_UINT : start_of_central_directory_location)
389
392
  # .ZIP file comment length 2 bytes
390
- io << [comment.bytesize].pack(C_UINT2)
393
+ buf.uint2(comment.bytesize)
391
394
  # .ZIP file comment (variable size)
392
- io << comment
395
+ buf.bytes(comment)
396
+ io << buf.b
393
397
  end
394
398
 
395
399
  private
@@ -400,17 +404,16 @@ class ZipKit::ZipWriter
400
404
  # @param uncompressed_size[Integer] The size of the file once extracted
401
405
  # @return [String]
402
406
  def zip_64_extra_for_local_file_header(compressed_size:, uncompressed_size:)
403
- data_and_packspecs = [
404
- # 2 bytes Tag for this "extra" block type
405
- 0x0001, C_UINT2,
406
- # 2 bytes Size of this "extra" block. For us it will always be 16 (2x8)
407
- 16, C_UINT2,
408
- # 8 bytes Original uncompressed file size
409
- uncompressed_size, C_UINT8,
410
- # 8 bytes Size of compressed data
411
- compressed_size, C_UINT8
412
- ]
413
- pack_array(data_and_packspecs)
407
+ buf = PackBuffer.new
408
+ # 2 bytes Tag for this "extra" block type
409
+ buf.uint2(0x0001)
410
+ # 2 bytes Size of this "extra" block. For us it will always be 16 (2x8)
411
+ buf.uint2(16)
412
+ # 8 bytes Original uncompressed file size
413
+ buf.uint8(uncompressed_size)
414
+ # 8 bytes Size of compressed data
415
+ buf.uint8(compressed_size)
416
+ buf.b
414
417
  end
415
418
 
416
419
  # Writes the extended timestamp information field for local headers.
@@ -449,18 +452,19 @@ class ZipKit::ZipWriter
449
452
  # bit 2 if set, creation time is present
450
453
  # bits 3-7 reserved for additional timestamps; not set
451
454
  flags = 0b00000001 # Set the lowest bit only, to indicate that only mtime is present
452
- data_and_packspecs = [
453
- # tag for this extra block type ("UT")
454
- 0x5455, C_UINT2,
455
- # the size of this block (1 byte used for the Flag + 3 longs used for the timestamp)
456
- (1 + 4), C_UINT2,
457
- # encode a single byte
458
- flags, C_CHAR,
459
- # Use a signed int, not the unsigned one used by the rest of the ZIP spec.
460
- mtime.utc.to_i, C_INT4
461
- ]
462
455
  # The atime and ctime can be omitted if not present
463
- pack_array(data_and_packspecs)
456
+ buf = PackBuffer.new
457
+ # tag for this extra block type ("UT")
458
+ buf.uint2(0x5455)
459
+ # the size of this block (1 byte used for the Flag + 3 longs used for the timestamp)
460
+ buf.uint2((1 + 4))
461
+ # encode a single byte
462
+ buf.uint1(flags)
463
+ # Use a signed int, not the unsigned one used by the rest of the ZIP spec.
464
+ # Time#utc would convert the given Time to UTC in-place, and the DOS time fields
465
+ # computed from it afterwards would then be in UTC instead of local time
466
+ buf.int4(mtime.to_i)
467
+ buf.b
464
468
  end
465
469
 
466
470
  # Since we do not supply atime or ctime, the contents of the two extra fields (central dir and local header)
@@ -475,21 +479,20 @@ class ZipKit::ZipWriter
475
479
  # @param local_file_header_location[Integer] Byte offset of the start of the local file header from the beginning of the ZIP archive
476
480
  # @return [String]
477
481
  def zip_64_extra_for_central_directory_file_header(compressed_size:, uncompressed_size:, local_file_header_location:)
478
- data_and_packspecs = [
479
- # 2 bytes Tag for this "extra" block type
480
- 0x0001, C_UINT2,
481
- # 2 bytes Size of this "extra" block. For us it will always be 28
482
- 28, C_UINT2,
483
- # 8 bytes Original uncompressed file size
484
- uncompressed_size, C_UINT8,
485
- # 8 bytes Size of compressed data
486
- compressed_size, C_UINT8,
487
- # 8 bytes Offset of local header record
488
- local_file_header_location, C_UINT8,
489
- # 4 bytes Number of the disk on which this file starts
490
- 0, C_UINT4
491
- ]
492
- pack_array(data_and_packspecs)
482
+ buf = PackBuffer.new
483
+ # 2 bytes Tag for this "extra" block type
484
+ buf.uint2(0x0001)
485
+ # 2 bytes Size of this "extra" block. For us it will always be 28
486
+ buf.uint2(28)
487
+ # 8 bytes Original uncompressed file size
488
+ buf.uint8(uncompressed_size)
489
+ # 8 bytes Size of compressed data
490
+ buf.uint8(compressed_size)
491
+ # 8 bytes Offset of local header record
492
+ buf.uint8(local_file_header_location)
493
+ # 4 bytes Number of the disk on which this file starts
494
+ buf.uint4(0)
495
+ buf.b
493
496
  end
494
497
 
495
498
  def to_binary_dos_time(t)
@@ -500,22 +503,6 @@ class ZipKit::ZipWriter
500
503
  t.day + (t.month << 5) + ((t.year - 1980) << 9)
501
504
  end
502
505
 
503
- # Unzips a given array of tuples of "numeric value, pack specifier" and then packs all the odd
504
- # values using specifiers from all the even values. It is harder to explain than to show:
505
- #
506
- # pack_array([1, 'V', 2, 'v', 148, 'v]) #=> "\x01\x00\x00\x00\x02\x00\x94\x00"
507
- #
508
- # will do the following two transforms:
509
- #
510
- # [1, 'V', 2, 'v', 148, 'v] -> [1,2,148], ['V','v','v'] -> [1,2,148].pack('Vvv') -> "\x01\x00\x00\x00\x02\x00\x94\x00".
511
- # This might seem like a "clever optimisation" but the issue is that `pack` needs an array allocated per call, and
512
- # we output very verbosely - value-by-value. This might be quite a few array allocs. Using something like this
513
- # helps us save the array allocs
514
- def pack_array(values_to_packspecs)
515
- values, packspecs = values_to_packspecs.partition.each_with_index { |_, i| i.even? }
516
- values.pack(packspecs.join)
517
- end
518
-
519
506
  def generate_external_attrs(unix_permissions_int, file_type_int)
520
507
  (file_type_int << 12 | (unix_permissions_int & 0o7777)) << 16
521
508
  end