zip_kit 6.4.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/CODE_OF_CONDUCT.md +12 -8
- data/CONTRIBUTING.md +56 -120
- data/README.md +1 -1
- data/examples/rack_application.rb +1 -1
- data/lib/zip_kit/file_reader.rb +91 -62
- data/lib/zip_kit/output_enumerator.rb +18 -11
- data/lib/zip_kit/rails_streaming.rb +15 -5
- data/lib/zip_kit/version.rb +1 -1
- data/lib/zip_kit/zip_writer.rb +207 -220
- data/rbi/zip_kit.rbi +68 -45
- data/rbi/zip_kit.rbs +58 -32
- metadata +2 -2
data/lib/zip_kit/zip_writer.rb
CHANGED
|
@@ -43,11 +43,64 @@ class ZipKit::ZipWriter
|
|
|
43
43
|
[VERSION_MADE_BY, os_type].pack("CC").freeze
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
145
|
+
buf.uint4(0x04034b50)
|
|
86
146
|
# version needed to extract 2 bytes
|
|
87
|
-
|
|
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
|
-
|
|
149
|
+
buf.uint2(gp_flags)
|
|
95
150
|
# compression method 2 bytes
|
|
96
|
-
|
|
151
|
+
buf.uint2(storage_mode)
|
|
97
152
|
# last mod file time 2 bytes
|
|
98
|
-
|
|
153
|
+
buf.uint2(to_binary_dos_time(mtime))
|
|
99
154
|
# last mod file date 2 bytes
|
|
100
|
-
|
|
155
|
+
buf.uint2(to_binary_dos_date(mtime))
|
|
101
156
|
# crc-32 4 bytes
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
118
|
-
|
|
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
|
-
|
|
133
|
-
|
|
166
|
+
buf.uint2(extra_fields.bytesize)
|
|
134
167
|
# file name (variable size)
|
|
135
|
-
|
|
136
|
-
|
|
168
|
+
buf.bytes(filename)
|
|
137
169
|
# Contents of the extra fields (variable size)
|
|
138
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
+
buf.uint2(gp_flags)
|
|
185
231
|
# compression method 2 bytes
|
|
186
|
-
|
|
232
|
+
buf.uint2(storage_mode)
|
|
187
233
|
# last mod file time 2 bytes
|
|
188
|
-
|
|
234
|
+
buf.uint2(to_binary_dos_time(mtime))
|
|
189
235
|
# last mod file date 2 bytes
|
|
190
|
-
|
|
236
|
+
buf.uint2(to_binary_dos_date(mtime))
|
|
191
237
|
# crc-32 4 bytes
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
# uncompressed size
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
+
buf.uint2(extra_fields.bytesize)
|
|
218
248
|
# file comment length 2 bytes
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
258
|
+
buf.uint4(external_attrs)
|
|
246
259
|
# relative offset of local header 4 bytes
|
|
247
|
-
|
|
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
|
-
|
|
262
|
+
buf.bytes(filename)
|
|
255
263
|
# extra field (variable size)
|
|
256
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
334
|
+
buf.uint8(44)
|
|
321
335
|
# version made by 2 bytes
|
|
322
|
-
|
|
336
|
+
buf.bytes(MADE_BY_SIGNATURE)
|
|
323
337
|
# version needed to extract 2 bytes
|
|
324
|
-
|
|
338
|
+
buf.uint2(VERSION_NEEDED_TO_EXTRACT_ZIP64)
|
|
325
339
|
# number of this disk 4 bytes
|
|
326
|
-
|
|
340
|
+
buf.uint4(0)
|
|
327
341
|
# number of the disk with the start of the central directory 4 bytes
|
|
328
|
-
|
|
342
|
+
buf.uint4(0)
|
|
329
343
|
# total number of entries in the
|
|
330
344
|
# central directory on this disk 8 bytes
|
|
331
|
-
|
|
345
|
+
buf.uint8(num_files_in_archive)
|
|
332
346
|
# total number of entries in the
|
|
333
347
|
# central directory 8 bytes
|
|
334
|
-
|
|
348
|
+
buf.uint8(num_files_in_archive)
|
|
335
349
|
# size of the central directory 8 bytes
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
+
buf.uint8(zip64_eocdr_offset)
|
|
353
367
|
# total number of disks 4 bytes
|
|
354
|
-
|
|
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
|
-
|
|
373
|
+
buf.uint4(0x06054b50)
|
|
360
374
|
# number of this disk 2 bytes
|
|
361
|
-
|
|
375
|
+
buf.uint2(0)
|
|
362
376
|
# number of the disk with the
|
|
363
377
|
# start of the central directory 2 bytes
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
+
buf.uint2(comment.bytesize)
|
|
391
394
|
# .ZIP file comment (variable size)
|
|
392
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|