zip_tricks 2.7.0 → 2.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/Rakefile +1 -0
- data/lib/zip_tricks/microzip.rb +367 -0
- data/lib/zip_tricks/streamer.rb +8 -36
- data/lib/zip_tricks.rb +1 -1
- data/spec/spec_helper.rb +107 -4
- data/spec/zip_tricks/microzip_interop_spec.rb +48 -0
- data/spec/zip_tricks/microzip_spec.rb +236 -0
- data/spec/zip_tricks/remote_io_spec.rb +3 -1
- data/spec/zip_tricks/stored_size_estimator_spec.rb +2 -2
- data/spec/zip_tricks/streamer_spec.rb +7 -18
- data/zip_tricks.gemspec +6 -3
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84302e70bc873418b8a458456a75e0da7b5bc67d
|
4
|
+
data.tar.gz: 06f81fc860d5cf77fdbd4b570602ae4984297d43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 932fe6e3a095f43996505c642fce11009efba4298302e958f5fdf7649cd3ca9fd28ab1de6a33d1e20be2955a7efd58b4d432486d3004ab70437a55413c27934d
|
7
|
+
data.tar.gz: b9d43dbf16156cd3c877a8353dd0dd4a34e4c8b2e43664e34c4c3f2562c3a9937390bf398b0278449bf338f89337c348f30da637414e61b17eab58a939b05fc5
|
data/.travis.yml
CHANGED
data/Rakefile
CHANGED
@@ -22,6 +22,7 @@ Jeweler::Tasks.new do |gem|
|
|
22
22
|
gem.description = %Q{Makes rubyzip stream, for real}
|
23
23
|
gem.email = "me@julik.nl"
|
24
24
|
gem.authors = ["Julik Tarkhanov"]
|
25
|
+
gem.files.exclude "testing/**/*"
|
25
26
|
# dependencies defined in Gemfile
|
26
27
|
end
|
27
28
|
Jeweler::RubygemsDotOrgTasks.new
|
@@ -0,0 +1,367 @@
|
|
1
|
+
# A replacement for RubyZip for streaming, with a couple of small differences.
|
2
|
+
# The first difference is that it is verbosely-written-to-the-spec and you can actually
|
3
|
+
# follow what is happening. It does not support quite a few fancy features of Rubyzip,
|
4
|
+
# but instead it can be digested in one reading, and has solid Zip64 support. It also does
|
5
|
+
# not attempt any tricks with Zip64 placeholder extra fields because the ZipTricks streaming
|
6
|
+
# engine assumes you _know_ how large your file is (both compressed and uncompressed) _and_
|
7
|
+
# you have the file's CRC32 checksum upfront.
|
8
|
+
#
|
9
|
+
# Just like Rubyzip it will switch to Zip64 automatically if required, but there is no global
|
10
|
+
# setting to enable that behavior - it is always on.
|
11
|
+
class ZipTricks::Microzip
|
12
|
+
STORED = 0
|
13
|
+
DEFLATED = 8
|
14
|
+
|
15
|
+
TooMuch = Class.new(StandardError)
|
16
|
+
DuplicateFilenames = Class.new(StandardError)
|
17
|
+
UnknownMode = Class.new(StandardError)
|
18
|
+
|
19
|
+
FOUR_BYTE_MAX_UINT = 0xFFFFFFFF
|
20
|
+
TWO_BYTE_MAX_UINT = 0xFFFF
|
21
|
+
|
22
|
+
VERSION_MADE_BY = 52
|
23
|
+
VERSION_NEEDED_TO_EXTRACT = 20
|
24
|
+
VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45
|
25
|
+
DEFAULT_EXTERNAL_ATTRS = begin
|
26
|
+
# These need to be set so that the unarchived files do not become executable on UNIX, for
|
27
|
+
# security purposes. Strictly speaking we would want to make this user-customizable,
|
28
|
+
# but for now just putting in sane defaults will do. For example, Trac with zipinfo does this:
|
29
|
+
# zipinfo.external_attr = 0644 << 16L # permissions -r-wr--r--.
|
30
|
+
# We snatch the incantations from Rubyzip for this.
|
31
|
+
unix_perms = 0644
|
32
|
+
file_type_file = 010
|
33
|
+
external_attrs = (file_type_file << 12 | (unix_perms & 07777)) << 16
|
34
|
+
end
|
35
|
+
MADE_BY_SIGNATURE = begin
|
36
|
+
# A combination of the VERSION_MADE_BY low byte and the OS type high byte
|
37
|
+
os_type = 3 # UNIX
|
38
|
+
[VERSION_MADE_BY, os_type].pack('CC')
|
39
|
+
end
|
40
|
+
|
41
|
+
C_V = 'V'.freeze
|
42
|
+
C_v = 'v'.freeze
|
43
|
+
C_Qe = 'Q<'.freeze
|
44
|
+
|
45
|
+
module Bytesize
|
46
|
+
def bytesize_of
|
47
|
+
''.force_encoding(Encoding::BINARY).tap {|b| yield(b) }.bytesize
|
48
|
+
end
|
49
|
+
end
|
50
|
+
include Bytesize
|
51
|
+
|
52
|
+
class Entry < Struct.new(:filename, :crc32, :compressed_size, :uncompressed_size, :storage_mode, :mtime)
|
53
|
+
include Bytesize
|
54
|
+
def initialize(*)
|
55
|
+
super
|
56
|
+
@requires_zip64 = (compressed_size > FOUR_BYTE_MAX_UINT || uncompressed_size > FOUR_BYTE_MAX_UINT)
|
57
|
+
if filename.bytesize > TWO_BYTE_MAX_UINT
|
58
|
+
raise TooMuch, "The given filename is too long to fit (%d bytes)" % filename.bytesize
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def requires_zip64?
|
63
|
+
@requires_zip64
|
64
|
+
end
|
65
|
+
|
66
|
+
# Set the general purpose flags for the entry. The only flag we care about is the EFS
|
67
|
+
# bit (bit 11) which should be set if the filename is UTF8. If it is, we need to set the
|
68
|
+
# bit so that the unarchiving application knows that the filename in the archive is UTF-8
|
69
|
+
# encoded, and not some DOS default. For ASCII entries it does not matter.
|
70
|
+
#
|
71
|
+
# Now, strictly speaking, if a diacritic-containing character (such as å) does fit into the DOS-437
|
72
|
+
# codepage, it should be encodable as such. This would, in theory, let older Windows tools
|
73
|
+
# decode the filename correctly. However, this kills the filename decoding for the OSX builtin
|
74
|
+
# archive utility (it assumes the filename to be UTF-8, regardless). So if we allow filenames
|
75
|
+
# to be encoded in DOS-437, we _potentially_ have support in Windows but we upset everyone on Mac.
|
76
|
+
# If we just use UTF-8 and set the right EFS bit in general purpose flags, we upset Windows users
|
77
|
+
# because most of the Windows unarchive tools (at least the builtin ones) do not give a flying eff
|
78
|
+
# about the EFS support bit being set.
|
79
|
+
#
|
80
|
+
# Additionally, if we use Unarchiver on OSX (which is our recommended unpacker for large files),
|
81
|
+
# it will (very rightfully) ask us how we should decode each filename that does not have the EFS bit,
|
82
|
+
# but does contain something non-ASCII-decodable. This is horrible UX for users.
|
83
|
+
#
|
84
|
+
# So, basically, we have 2 choices, for filenames containing diacritics (for bona-fide UTF-8 you do not
|
85
|
+
# even get those choices, you _have_ to use UTF-8):
|
86
|
+
#
|
87
|
+
# * Make life easier for Windows users by setting stuff to DOS, not care about the standard _and_ make
|
88
|
+
# most of Mac users upset
|
89
|
+
# * Make life easy for Mac users and conform to the standard, and tell Windows users to get a _decent_
|
90
|
+
# ZIP unarchiving tool.
|
91
|
+
#
|
92
|
+
# We are going with option 2, and this is well-thought-out. Trust me. If you want the crazytown
|
93
|
+
# filename encoding scheme that is described here http://stackoverflow.com/questions/13261347
|
94
|
+
# you can try this:
|
95
|
+
#
|
96
|
+
# [Encoding::CP437, Encoding::ISO_8859_1, Encoding::UTF_8]
|
97
|
+
#
|
98
|
+
# We don't want no such thing, and sorry Windows users, you are going to need a decent unarchiver
|
99
|
+
# that honors the standard. Alas, alas.
|
100
|
+
def gp_flags_based_on_filename
|
101
|
+
filename.encode(Encoding::ASCII)
|
102
|
+
0b00000000000
|
103
|
+
rescue EncodingError
|
104
|
+
0b00000000000 | 0b100000000000
|
105
|
+
end
|
106
|
+
|
107
|
+
def write_local_file_header(io)
|
108
|
+
# TBD: caveat. If this entry _does_ fit into a standard zip segment (both compressed and
|
109
|
+
# uncompressed size at or below 0xFFFF etc), but it is _located_ at an offset that requires
|
110
|
+
# Zip64 to be used (beyound 4GB), we are going to be omitting the Zip64 extras in the local
|
111
|
+
# file header, but we will be enabling them when writing the central directory. Then the
|
112
|
+
# CD record for the file _will_ have Zip64 extra, but the local file header won't. In theory,
|
113
|
+
# this should not pose a problem, but then again... life in this world can be harsh.
|
114
|
+
#
|
115
|
+
# If it turns out that it _does_ pose a problem, we can always do:
|
116
|
+
#
|
117
|
+
# @requires_zip64 = true if io.tell > FOUR_BYTE_MAX_UINT
|
118
|
+
#
|
119
|
+
# right here, and have the data written regardless even if the file fits.
|
120
|
+
io << [0x04034b50].pack(C_V) # local file header signature 4 bytes (0x04034b50)
|
121
|
+
|
122
|
+
if @requires_zip64 # version needed to extract 2 bytes
|
123
|
+
io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_v)
|
124
|
+
else
|
125
|
+
io << [VERSION_NEEDED_TO_EXTRACT].pack(C_v)
|
126
|
+
end
|
127
|
+
|
128
|
+
io << [gp_flags_based_on_filename].pack("v") # general purpose bit flag 2 bytes
|
129
|
+
io << [storage_mode].pack("v") # compression method 2 bytes
|
130
|
+
io << [to_binary_dos_time(mtime)].pack(C_v) # last mod file time 2 bytes
|
131
|
+
io << [to_binary_dos_date(mtime)].pack(C_v) # last mod file date 2 bytes
|
132
|
+
io << [crc32].pack(C_V) # crc-32 4 bytes
|
133
|
+
|
134
|
+
if @requires_zip64
|
135
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_V) # compressed size 4 bytes
|
136
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_V) # uncompressed size 4 bytes
|
137
|
+
else
|
138
|
+
io << [compressed_size].pack(C_V) # compressed size 4 bytes
|
139
|
+
io << [uncompressed_size].pack(C_V) # uncompressed size 4 bytes
|
140
|
+
end
|
141
|
+
|
142
|
+
# Filename should not be longer than 0xFFFF otherwise this wont fit here
|
143
|
+
io << [filename.bytesize].pack(C_v) # file name length 2 bytes
|
144
|
+
|
145
|
+
extra_size = 0
|
146
|
+
if @requires_zip64
|
147
|
+
extra_size += bytesize_of {|buf| write_zip_64_extra_for_local_file_header(buf) }
|
148
|
+
end
|
149
|
+
io << [extra_size].pack(C_v) # extra field length 2 bytes
|
150
|
+
|
151
|
+
io << filename # file name (variable size)
|
152
|
+
|
153
|
+
# Interesting tidbit:
|
154
|
+
# https://social.technet.microsoft.com/Forums/windows/en-US/6a60399f-2879-4859-b7ab-6ddd08a70948
|
155
|
+
# TL;DR of it is: Windows 7 Explorer _will_ open Zip64 entries. However, it desires to have the
|
156
|
+
# Zip64 extra field as _the first_ extra field. If we decide to add the Info-ZIP UTF-8 field...
|
157
|
+
write_zip_64_extra_for_local_file_header(io) if @requires_zip64
|
158
|
+
end
|
159
|
+
|
160
|
+
def write_zip_64_extra_for_local_file_header(io)
|
161
|
+
io << [0x0001].pack(C_v) # 2 bytes Tag for this "extra" block type
|
162
|
+
io << [16].pack(C_v) # 2 bytes Size of this "extra" block. For us it will always be 16 (2x8)
|
163
|
+
io << [uncompressed_size].pack(C_Qe) # 8 bytes Original uncompressed file size
|
164
|
+
io << [compressed_size].pack(C_Qe) # 8 bytes Size of compressed data
|
165
|
+
end
|
166
|
+
|
167
|
+
def write_zip_64_extra_for_central_directory_file_header(io, local_file_header_location)
|
168
|
+
io << [0x0001].pack(C_v) # 2 bytes Tag for this "extra" block type
|
169
|
+
io << [28].pack(C_v) # 2 bytes Size of this "extra" block. For us it will always be 28
|
170
|
+
io << [uncompressed_size].pack(C_Qe) # 8 bytes Original uncompressed file size
|
171
|
+
io << [compressed_size].pack(C_Qe) # 8 bytes Size of compressed data
|
172
|
+
io << [local_file_header_location].pack(C_Qe) # 8 bytes Offset of local header record
|
173
|
+
io << [0].pack(C_V) # 4 bytes Number of the disk on which this file starts
|
174
|
+
end
|
175
|
+
|
176
|
+
def write_central_directory_file_header(io, local_file_header_location)
|
177
|
+
# At this point if the header begins somewhere beyound 0xFFFFFFFF we _have_ to record the offset
|
178
|
+
# of the local file header as a zip64 extra field, so we give up, give in, you loose, love will always win...
|
179
|
+
@requires_zip64 = true if local_file_header_location > FOUR_BYTE_MAX_UINT
|
180
|
+
|
181
|
+
io << [0x02014b50].pack(C_V) # central file header signature 4 bytes (0x02014b50)
|
182
|
+
io << MADE_BY_SIGNATURE # version made by 2 bytes
|
183
|
+
if @requires_zip64
|
184
|
+
io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_v) # version needed to extract 2 bytes
|
185
|
+
else
|
186
|
+
io << [VERSION_NEEDED_TO_EXTRACT].pack(C_v) # version needed to extract 2 bytes
|
187
|
+
end
|
188
|
+
|
189
|
+
io << [gp_flags_based_on_filename].pack(C_v) # general purpose bit flag 2 bytes
|
190
|
+
io << [storage_mode].pack(C_v) # compression method 2 bytes
|
191
|
+
io << [to_binary_dos_time(mtime)].pack(C_v) # last mod file time 2 bytes
|
192
|
+
io << [to_binary_dos_date(mtime)].pack(C_v) # last mod file date 2 bytes
|
193
|
+
io << [crc32].pack(C_V) # crc-32 4 bytes
|
194
|
+
|
195
|
+
if @requires_zip64
|
196
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_V) # compressed size 4 bytes
|
197
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_V) # uncompressed size 4 bytes
|
198
|
+
else
|
199
|
+
io << [compressed_size].pack(C_V) # compressed size 4 bytes
|
200
|
+
io << [uncompressed_size].pack(C_V) # uncompressed size 4 bytes
|
201
|
+
end
|
202
|
+
|
203
|
+
# Filename should not be longer than 0xFFFF otherwise this wont fit here
|
204
|
+
io << [filename.bytesize].pack(C_v) # file name length 2 bytes
|
205
|
+
|
206
|
+
extra_size = 0
|
207
|
+
if @requires_zip64
|
208
|
+
extra_size += bytesize_of {|buf|
|
209
|
+
write_zip_64_extra_for_central_directory_file_header(buf, local_file_header_location)
|
210
|
+
}
|
211
|
+
end
|
212
|
+
io << [extra_size].pack(C_v) # extra field length 2 bytes
|
213
|
+
|
214
|
+
io << [0].pack(C_v) # file comment length 2 bytes
|
215
|
+
io << [0].pack(C_v) # disk number start 2 bytes
|
216
|
+
io << [0].pack(C_v) # internal file attributes 2 bytes
|
217
|
+
|
218
|
+
io << [DEFAULT_EXTERNAL_ATTRS].pack(C_V) # external file attributes 4 bytes
|
219
|
+
|
220
|
+
if @requires_zip64
|
221
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_V) # relative offset of local header 4 bytes
|
222
|
+
else
|
223
|
+
io << [local_file_header_location].pack(C_V) # relative offset of local header 4 bytes
|
224
|
+
end
|
225
|
+
io << filename # file name (variable size)
|
226
|
+
|
227
|
+
if @requires_zip64 # extra field (variable size)
|
228
|
+
write_zip_64_extra_for_central_directory_file_header(io, local_file_header_location)
|
229
|
+
end
|
230
|
+
# file comment (variable size)
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
def to_binary_dos_time(t)
|
236
|
+
(t.sec/2) + (t.min << 5) + (t.hour << 11)
|
237
|
+
end
|
238
|
+
|
239
|
+
def to_binary_dos_date(t)
|
240
|
+
(t.day) + (t.month << 5) + ((t.year - 1980) << 9)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Creates a new streaming writer.
|
245
|
+
# The writer is stateful and knows it's list of ZIP file entries as they are being added.
|
246
|
+
def initialize
|
247
|
+
@files = []
|
248
|
+
@local_header_offsets = []
|
249
|
+
end
|
250
|
+
|
251
|
+
# Adds a file to the entry list and immediately writes out it's local file header into the
|
252
|
+
# output stream.
|
253
|
+
#
|
254
|
+
# @param io[#<<, #tell] the buffer to write the local file header to
|
255
|
+
# @param filename[String] The name of the file
|
256
|
+
# @param crc32[Fixnum] The CRC32 checksum of the file
|
257
|
+
# @param compressed_size[Fixnum] The size of the compressed (or stored) data - how much space it uses in the ZIP
|
258
|
+
# @param uncompressed_size[Fixnum] The size of the file once extracted
|
259
|
+
# @param storage_mode[Fixnum] Either 0 for "stored" or 8 for "deflated"
|
260
|
+
# @param mtime[Time] What modification time to record for the file
|
261
|
+
# @return [void]
|
262
|
+
def add_local_file_header(io:, filename:, crc32:, compressed_size:, uncompressed_size:, storage_mode:, mtime: Time.now.utc)
|
263
|
+
if @files.any?{|e| e.filename == filename }
|
264
|
+
raise DuplicateFilenames, "Filename #{filename.inspect} already used in the archive"
|
265
|
+
end
|
266
|
+
raise UnknownMode, "Unknown compression mode #{storage_mode}" unless [STORED, DEFLATED].include?(storage_mode)
|
267
|
+
e = Entry.new(filename, crc32, compressed_size, uncompressed_size, storage_mode, mtime)
|
268
|
+
@files << e
|
269
|
+
@local_header_offsets << io.tell
|
270
|
+
e.write_local_file_header(io)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Writes the central directory (including the Zip6 salient bits if necessary)
|
274
|
+
#
|
275
|
+
# @param io[#<<, #tell] the buffer to write the central directory to.
|
276
|
+
# The method will use `tell` on the buffer since it has to know where the central directory is located
|
277
|
+
# @return [void]
|
278
|
+
def write_central_directory(io)
|
279
|
+
start_of_central_directory = io.tell
|
280
|
+
|
281
|
+
# Central directory file headers, per file in order
|
282
|
+
@files.each_with_index do |file, i|
|
283
|
+
local_file_header_offset_from_start_of_file = @local_header_offsets.fetch(i)
|
284
|
+
file.write_central_directory_file_header(io, local_file_header_offset_from_start_of_file)
|
285
|
+
end
|
286
|
+
central_dir_size = io.tell - start_of_central_directory
|
287
|
+
|
288
|
+
zip64_required = central_dir_size > FOUR_BYTE_MAX_UINT ||
|
289
|
+
start_of_central_directory > FOUR_BYTE_MAX_UINT ||
|
290
|
+
@files.length > TWO_BYTE_MAX_UINT ||
|
291
|
+
@files.any?(&:requires_zip64?)
|
292
|
+
|
293
|
+
# Then, if zip64 is used
|
294
|
+
if zip64_required
|
295
|
+
# [zip64 end of central directory record]
|
296
|
+
zip64_eocdr_offset = io.tell
|
297
|
+
# zip64 end of central dir
|
298
|
+
io << [0x06064b50].pack(C_V) # signature 4 bytes (0x06064b50)
|
299
|
+
io << [44].pack(C_Qe) # size of zip64 end of central
|
300
|
+
# directory record 8 bytes
|
301
|
+
# (this is ex. the 12 bytes of the signature and the size value itself).
|
302
|
+
# Without the extensible data sector it is always 44.
|
303
|
+
io << MADE_BY_SIGNATURE # version made by 2 bytes
|
304
|
+
io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_v) # version needed to extract 2 bytes
|
305
|
+
io << [0].pack(C_V) # number of this disk 4 bytes
|
306
|
+
io << [0].pack(C_V) # number of the disk with the
|
307
|
+
# start of the central directory 4 bytes
|
308
|
+
io << [@files.length].pack(C_Qe) # total number of entries in the
|
309
|
+
# central directory on this disk 8 bytes
|
310
|
+
io << [@files.length].pack(C_Qe) # total number of entries in the
|
311
|
+
# central directory 8 bytes
|
312
|
+
io << [central_dir_size].pack(C_Qe) # size of the central directory 8 bytes
|
313
|
+
# offset of start of central
|
314
|
+
# directory with respect to
|
315
|
+
io << [start_of_central_directory].pack(C_Qe) # the starting disk number 8 bytes
|
316
|
+
# zip64 extensible data sector (variable size)
|
317
|
+
|
318
|
+
# [zip64 end of central directory locator]
|
319
|
+
io << [0x07064b50].pack("V") # zip64 end of central dir locator
|
320
|
+
# signature 4 bytes (0x07064b50)
|
321
|
+
io << [0].pack(C_V) # number of the disk with the
|
322
|
+
# start of the zip64 end of
|
323
|
+
# central directory 4 bytes
|
324
|
+
io << [zip64_eocdr_offset].pack(C_Qe) # relative offset of the zip64
|
325
|
+
# end of central directory record 8 bytes
|
326
|
+
# (note: "relative" is actually "from the start of the file")
|
327
|
+
io << [1].pack(C_V) # total number of disks 4 bytes
|
328
|
+
end
|
329
|
+
|
330
|
+
# Then the end of central directory record:
|
331
|
+
io << [0x06054b50].pack(C_V) # end of central dir signature 4 bytes (0x06054b50)
|
332
|
+
io << [0].pack(C_v) # number of this disk 2 bytes
|
333
|
+
io << [0].pack(C_v) # number of the disk with the
|
334
|
+
# start of the central directory 2 bytes
|
335
|
+
|
336
|
+
if zip64_required # the number of entries will be read from the zip64 part of the central directory
|
337
|
+
io << [TWO_BYTE_MAX_UINT].pack(C_v) # total number of entries in the
|
338
|
+
# central directory on this disk 2 bytes
|
339
|
+
io << [TWO_BYTE_MAX_UINT].pack(C_v) # total number of entries in
|
340
|
+
# the central directory 2 bytes
|
341
|
+
else
|
342
|
+
io << [@files.length].pack(C_v) # total number of entries in the
|
343
|
+
# central directory on this disk 2 bytes
|
344
|
+
io << [@files.length].pack(C_v) # total number of entries in
|
345
|
+
# the central directory 2 bytes
|
346
|
+
end
|
347
|
+
|
348
|
+
if zip64_required
|
349
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_V) # size of the central directory 4 bytes
|
350
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_V) # offset of start of central
|
351
|
+
# directory with respect to
|
352
|
+
# the starting disk number 4 bytes
|
353
|
+
else
|
354
|
+
io << [central_dir_size].pack(C_V) # size of the central directory 4 bytes
|
355
|
+
io << [start_of_central_directory].pack(C_V) # offset of start of central
|
356
|
+
# directory with respect to
|
357
|
+
# the starting disk number 4 bytes
|
358
|
+
end
|
359
|
+
io << [0].pack(C_v) # .ZIP file comment length 2 bytes
|
360
|
+
# .ZIP file comment (variable size)
|
361
|
+
end
|
362
|
+
|
363
|
+
private_constant :FOUR_BYTE_MAX_UINT, :TWO_BYTE_MAX_UINT,
|
364
|
+
:VERSION_MADE_BY, :VERSION_NEEDED_TO_EXTRACT, :VERSION_NEEDED_TO_EXTRACT_ZIP64,
|
365
|
+
:DEFAULT_EXTERNAL_ATTRS, :MADE_BY_SIGNATURE,
|
366
|
+
:Entry, :C_V, :C_v, :C_Qe
|
367
|
+
end
|
data/lib/zip_tricks/streamer.rb
CHANGED
@@ -38,8 +38,10 @@ class ZipTricks::Streamer
|
|
38
38
|
def initialize(stream)
|
39
39
|
raise InvalidOutput, "The stream should respond to #<<" unless stream.respond_to?(:<<)
|
40
40
|
stream = ZipTricks::WriteAndTell.new(stream) unless stream.respond_to?(:tell) && stream.respond_to?(:advance_position_by)
|
41
|
+
|
41
42
|
@output_stream = stream
|
42
|
-
|
43
|
+
@zip = ZipTricks::Microzip.new
|
44
|
+
|
43
45
|
@state_monitor = VeryTinyStateMachine.new(:before_entry, callbacks_to=self)
|
44
46
|
@state_monitor.permit_state :in_entry_header, :in_entry_body, :in_central_directory, :closed
|
45
47
|
@state_monitor.permit_transition :before_entry => :in_entry_header
|
@@ -47,8 +49,6 @@ class ZipTricks::Streamer
|
|
47
49
|
@state_monitor.permit_transition :in_entry_body => :in_entry_header
|
48
50
|
@state_monitor.permit_transition :in_entry_body => :in_central_directory
|
49
51
|
@state_monitor.permit_transition :in_central_directory => :closed
|
50
|
-
|
51
|
-
@entry_set = ::Zip::EntrySet.new
|
52
52
|
end
|
53
53
|
|
54
54
|
# Writes a part of a zip entry body (actual binary data of the entry) into the output stream.
|
@@ -99,16 +99,8 @@ class ZipTricks::Streamer
|
|
99
99
|
# @return [Fixnum] the offset the output IO is at after writing the entry header
|
100
100
|
def add_compressed_entry(entry_name, uncompressed_size, crc32, compressed_size)
|
101
101
|
@state_monitor.transition! :in_entry_header
|
102
|
-
|
103
|
-
|
104
|
-
entry.compression_method = Zip::Entry::DEFLATED
|
105
|
-
entry.crc = crc32
|
106
|
-
entry.size = uncompressed_size
|
107
|
-
entry.compressed_size = compressed_size
|
108
|
-
set_gp_flags_for_filename(entry, entry_name)
|
109
|
-
|
110
|
-
@entry_set << entry
|
111
|
-
entry.write_local_entry(@output_stream)
|
102
|
+
@zip.add_local_file_header(io: @output_stream, filename: entry_name, crc32: crc32,
|
103
|
+
compressed_size: compressed_size, uncompressed_size: uncompressed_size, storage_mode: ZipTricks::Microzip::DEFLATED)
|
112
104
|
@expected_bytes_for_entry = compressed_size
|
113
105
|
@bytes_written_for_entry = 0
|
114
106
|
@output_stream.tell
|
@@ -123,21 +115,13 @@ class ZipTricks::Streamer
|
|
123
115
|
# @return [Fixnum] the offset the output IO is at after writing the entry header
|
124
116
|
def add_stored_entry(entry_name, uncompressed_size, crc32)
|
125
117
|
@state_monitor.transition! :in_entry_header
|
126
|
-
|
127
|
-
|
128
|
-
entry.compression_method = Zip::Entry::STORED
|
129
|
-
entry.crc = crc32
|
130
|
-
entry.size = uncompressed_size
|
131
|
-
entry.compressed_size = uncompressed_size
|
132
|
-
set_gp_flags_for_filename(entry, entry_name)
|
133
|
-
@entry_set << entry
|
134
|
-
entry.write_local_entry(@output_stream)
|
118
|
+
@zip.add_local_file_header(io: @output_stream, filename: entry_name, crc32: crc32,
|
119
|
+
compressed_size: uncompressed_size, uncompressed_size: uncompressed_size, storage_mode: ZipTricks::Microzip::STORED)
|
135
120
|
@bytes_written_for_entry = 0
|
136
121
|
@expected_bytes_for_entry = uncompressed_size
|
137
122
|
@output_stream.tell
|
138
123
|
end
|
139
124
|
|
140
|
-
|
141
125
|
# Writes out the global footer and the directory entry header and the global directory of the ZIP
|
142
126
|
# archive using the information about the entries added using `add_stored_entry` and `add_compressed_entry`.
|
143
127
|
#
|
@@ -146,8 +130,7 @@ class ZipTricks::Streamer
|
|
146
130
|
# @return [Fixnum] the offset the output IO is at after writing the central directory
|
147
131
|
def write_central_directory!
|
148
132
|
@state_monitor.transition! :in_central_directory
|
149
|
-
|
150
|
-
cdir.write_to_stream(@output_stream)
|
133
|
+
@zip.write_central_directory(@output_stream)
|
151
134
|
@output_stream.tell
|
152
135
|
end
|
153
136
|
|
@@ -165,17 +148,6 @@ class ZipTricks::Streamer
|
|
165
148
|
|
166
149
|
private
|
167
150
|
|
168
|
-
# Set the general purpose flags for the entry. The only flag we care about is the EFS
|
169
|
-
# bit (bit 11) which should be set if the filename is UTF8. If it is, we need to set the
|
170
|
-
# bit so that the unarchiving application knows that the filename in the archive is UTF-8
|
171
|
-
# encoded, and not some DOS default. For ASCII entries it does not matter.
|
172
|
-
def set_gp_flags_for_filename(entry, filename)
|
173
|
-
filename.encode(Encoding::ASCII)
|
174
|
-
entry.gp_flags = DEFAULT_GP_FLAGS
|
175
|
-
rescue Encoding::UndefinedConversionError #=> UTF8 filename
|
176
|
-
entry.gp_flags = DEFAULT_GP_FLAGS | EFS
|
177
|
-
end
|
178
|
-
|
179
151
|
# Checks whether the number of bytes written conforms to the declared entry size
|
180
152
|
def leaving_in_entry_body_state
|
181
153
|
if @bytes_written_for_entry != @expected_bytes_for_entry
|
data/lib/zip_tricks.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -4,11 +4,114 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
4
4
|
require 'rspec'
|
5
5
|
require 'zip_tricks'
|
6
6
|
require 'digest'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'shellwords'
|
7
9
|
|
8
|
-
|
9
|
-
#
|
10
|
-
|
10
|
+
module Keepalive
|
11
|
+
# Travis-CI kills the build if it does not receive output on standard out or standard error
|
12
|
+
# for longer than a few minutes. We have a few tests that take a _very_ long time, and during
|
13
|
+
# those tests this method has to be called every now and then to revive the output and let the
|
14
|
+
# build proceed.
|
15
|
+
def still_alive!
|
16
|
+
$keepalive_last_out_ping_at ||= Time.now
|
17
|
+
if (Time.now - $keepalive_last_out_ping_at) > 3
|
18
|
+
$keepalive_last_out_ping_at = Time.now
|
19
|
+
$stdout << '_'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
extend self
|
23
|
+
end
|
11
24
|
|
12
|
-
RSpec.configure do |config|
|
13
25
|
|
26
|
+
class ManagedTempfile < Tempfile
|
27
|
+
@@managed_tempfiles = []
|
28
|
+
|
29
|
+
def initialize(*)
|
30
|
+
super
|
31
|
+
@@managed_tempfiles << self
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.prune!
|
35
|
+
@@managed_tempfiles.each do |tf|
|
36
|
+
(tf.close; tf.unlink) rescue nil
|
37
|
+
end
|
38
|
+
@@managed_tempfiles.clear
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# A Tempfile filled with N bytes of random data, that also knows the CRC32 of that data
|
43
|
+
class RandomFile < ManagedTempfile
|
44
|
+
attr_reader :crc32
|
45
|
+
RANDOM_MEG = Random.new.bytes(1024 * 1024) # Allocate it once to prevent heap churn
|
46
|
+
def initialize(size)
|
47
|
+
super('random-bin')
|
48
|
+
binmode
|
49
|
+
crc = ZipTricks::StreamCRC32.new
|
50
|
+
bytes = size % (1024 * 1024)
|
51
|
+
megs = size / (1024 * 1024)
|
52
|
+
megs.times do
|
53
|
+
Keepalive.still_alive!
|
54
|
+
self << RANDOM_MEG
|
55
|
+
crc << RANDOM_MEG
|
56
|
+
end
|
57
|
+
random_blob = Random.new.bytes(bytes)
|
58
|
+
self << random_blob
|
59
|
+
crc << random_blob
|
60
|
+
@crc32 = crc.to_i
|
61
|
+
rewind
|
62
|
+
end
|
63
|
+
|
64
|
+
def copy_to(io)
|
65
|
+
rewind
|
66
|
+
while data = read(10*1024*1024)
|
67
|
+
io << data
|
68
|
+
Keepalive.still_alive!
|
69
|
+
end
|
70
|
+
rewind
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module ZipInspection
|
75
|
+
def inspect_zip_with_external_tool(path_to_zip)
|
76
|
+
zipinfo_path = 'zipinfo'
|
77
|
+
$zip_inspection_buf ||= StringIO.new
|
78
|
+
$zip_inspection_buf.puts "\n"
|
79
|
+
$zip_inspection_buf.puts "Inspecting ZIP output of #{inspect}." # The only way to get at the RSpec example without using the block argument
|
80
|
+
$zip_inspection_buf.puts "Be aware that the zipinfo version on OSX is too old to deal with Zip6."
|
81
|
+
escaped_cmd = Shellwords.join([zipinfo_path, '-tlhvz', path_to_zip])
|
82
|
+
$zip_inspection_buf.puts `#{escaped_cmd}`
|
83
|
+
end
|
84
|
+
|
85
|
+
def open_with_external_app(app_path, path_to_zip, skip_if_missing)
|
86
|
+
bin_exists = File.exist?(app_path)
|
87
|
+
skip "This system does not have #{File.basename(app_path)}" if skip_if_missing && !bin_exists
|
88
|
+
return unless bin_exists
|
89
|
+
`#{Shellwords.join([app_path, path_to_zip])}`
|
90
|
+
end
|
91
|
+
|
92
|
+
def open_zip_with_archive_utility(path_to_zip, skip_if_missing: false)
|
93
|
+
# ArchiveUtility sometimes puts the stuff it unarchives in ~/Downloads etc. so do
|
94
|
+
# not perform any checks on the files since we do not really know where they are on disk.
|
95
|
+
# Visual inspection should show whether the unarchiving is handled correctly.
|
96
|
+
au_path = '/System/Library/CoreServices/Applications/Archive Utility.app/Contents/MacOS/Archive Utility'
|
97
|
+
open_with_external_app(au_path, path_to_zip, skip_if_missing)
|
98
|
+
end
|
99
|
+
|
100
|
+
def open_zip_with_unarchiver(path_to_zip, skip_if_missing: false)
|
101
|
+
ua_path = '/Applications/The Unarchiver.app/Contents/MacOS/The Unarchiver'
|
102
|
+
open_with_external_app(ua_path, path_to_zip, skip_if_missing)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
RSpec.configure do |config|
|
107
|
+
config.include Keepalive
|
108
|
+
config.include ZipInspection
|
109
|
+
|
110
|
+
config.after :each do
|
111
|
+
ManagedTempfile.prune!
|
112
|
+
end
|
113
|
+
|
114
|
+
config.after :suite do
|
115
|
+
$stderr << $zip_inspection_buf.string if $zip_inspection_buf
|
116
|
+
end
|
14
117
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe 'Microzip in interop context' do
|
4
|
+
let(:described_class) { ZipTricks::Microzip}
|
5
|
+
|
6
|
+
it 'creates an archive that can be opened by Rubyzip, with a small number of very tiny text files' do
|
7
|
+
tf = ManagedTempfile.new('zip')
|
8
|
+
z = described_class.new
|
9
|
+
|
10
|
+
test_str = Random.new.bytes(64)
|
11
|
+
crc = Zlib.crc32(test_str)
|
12
|
+
t = Time.now.utc
|
13
|
+
|
14
|
+
3.times do |i|
|
15
|
+
fn = "test-#{i}"
|
16
|
+
z.add_local_file_header(io: tf, filename: fn, crc32: crc, compressed_size: test_str.bytesize,
|
17
|
+
uncompressed_size: test_str.bytesize, storage_mode: 0, mtime: t)
|
18
|
+
tf << test_str
|
19
|
+
end
|
20
|
+
z.write_central_directory(tf)
|
21
|
+
tf.flush
|
22
|
+
|
23
|
+
Zip::File.open(tf.path) do |zip_file|
|
24
|
+
entries = zip_file.to_a
|
25
|
+
expect(entries.length).to eq(3)
|
26
|
+
entries.each do |entry|
|
27
|
+
# Make sure it is tagged as UNIX
|
28
|
+
expect(entry.fstype).to eq(3)
|
29
|
+
|
30
|
+
# Check the file contents
|
31
|
+
readback = entry.get_input_stream.read
|
32
|
+
readback.force_encoding(Encoding::BINARY)
|
33
|
+
expect(readback).to eq(test_str)
|
34
|
+
|
35
|
+
# The CRC
|
36
|
+
expect(entry.crc).to eq(crc)
|
37
|
+
|
38
|
+
# Check the name
|
39
|
+
expect(entry.name).to match(/test/)
|
40
|
+
|
41
|
+
# Check the right external attributes (non-executable on UNIX)
|
42
|
+
expect(entry.external_file_attributes).to eq(2175008768)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
inspect_zip_with_external_tool(tf.path)
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::Microzip do
|
4
|
+
class ByteReader < Struct.new(:io)
|
5
|
+
def read_2b
|
6
|
+
io.read(2).unpack('v').first
|
7
|
+
end
|
8
|
+
|
9
|
+
def read_2c
|
10
|
+
io.read(2).unpack('CC').first
|
11
|
+
end
|
12
|
+
|
13
|
+
def read_4b
|
14
|
+
io.read(4).unpack('V').first
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_8b
|
18
|
+
io.read(8).unpack('Q<').first
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_n(n)
|
22
|
+
io.read(n)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'raises an exception if the filename is non-unique in the already existing set' do
|
27
|
+
z = described_class.new
|
28
|
+
z.add_local_file_header(io: StringIO.new, filename: 'foo.txt', crc32: 0, compressed_size: 0, uncompressed_size: 0, storage_mode: 0)
|
29
|
+
expect {
|
30
|
+
z.add_local_file_header(io: StringIO.new, filename: 'foo.txt', crc32: 0, compressed_size: 0, uncompressed_size: 0, storage_mode: 0)
|
31
|
+
}.to raise_error(/already/)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'raises an exception if the filename does not fit in 0xFFFF bytes' do
|
35
|
+
longest_filename_in_the_universe = "x" * (0xFFFF + 1)
|
36
|
+
z = described_class.new
|
37
|
+
expect {
|
38
|
+
z.add_local_file_header(io: StringIO.new, filename: longest_filename_in_the_universe,
|
39
|
+
crc32: 0, compressed_size: 0, uncompressed_size: 0, storage_mode: 0)
|
40
|
+
}.to raise_error(/filename/)
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#add_local_file_header' do
|
44
|
+
it 'writes out the local file header for an entry that fits into a standard ZIP' do
|
45
|
+
buf = StringIO.new
|
46
|
+
zip = described_class.new
|
47
|
+
mtime = Time.utc(2016, 7, 17, 13, 48)
|
48
|
+
zip.add_local_file_header(io: buf, filename: 'first-file.bin', crc32: 123, compressed_size: 8981,
|
49
|
+
uncompressed_size: 90981, storage_mode: 8, mtime: mtime)
|
50
|
+
|
51
|
+
buf.rewind
|
52
|
+
br = ByteReader.new(buf)
|
53
|
+
expect(br.read_4b).to eq(0x04034b50) # Signature
|
54
|
+
expect(br.read_2b).to eq(20) # Version needed to extract
|
55
|
+
expect(br.read_2b).to eq(0) # gp flags
|
56
|
+
expect(br.read_2b).to eq(8) # storage mode
|
57
|
+
expect(br.read_2b).to eq(28160) # DOS time
|
58
|
+
expect(br.read_2b).to eq(18673) # DOS date
|
59
|
+
expect(br.read_4b).to eq(123) # CRC32
|
60
|
+
expect(br.read_4b).to eq(8981) # compressed size
|
61
|
+
expect(br.read_4b).to eq(90981) # uncompressed size
|
62
|
+
expect(br.read_2b).to eq('first-file.bin'.bytesize) # byte length of the filename
|
63
|
+
expect(br.read_2b).to be_zero # size of extra fields
|
64
|
+
expect(br.read_n('first-file.bin'.bytesize)).to eq('first-file.bin') # the filename
|
65
|
+
expect(buf).to be_eof
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'writes out the local file header for an entry with a UTF-8 filename, setting the proper GP flag bit' do
|
69
|
+
buf = StringIO.new
|
70
|
+
zip = described_class.new
|
71
|
+
mtime = Time.utc(2016, 7, 17, 13, 48)
|
72
|
+
zip.add_local_file_header(io: buf, filename: 'файл.bin', crc32: 123, compressed_size: 8981,
|
73
|
+
uncompressed_size: 90981, storage_mode: 8, mtime: mtime)
|
74
|
+
|
75
|
+
buf.rewind
|
76
|
+
br = ByteReader.new(buf)
|
77
|
+
br.read_4b # Signature
|
78
|
+
br.read_2b # Version needed to extract
|
79
|
+
expect(br.read_2b).to eq(2048) # gp flags
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'writes out the local file header for an entry with a filename with diacritics, setting the proper GP flag bit' do
|
83
|
+
buf = StringIO.new
|
84
|
+
zip = described_class.new
|
85
|
+
mtime = Time.utc(2016, 7, 17, 13, 48)
|
86
|
+
zip.add_local_file_header(io: buf, filename: 'Kungälv', crc32: 123, compressed_size: 8981,
|
87
|
+
uncompressed_size: 90981, storage_mode: 8, mtime: mtime)
|
88
|
+
|
89
|
+
buf.rewind
|
90
|
+
br = ByteReader.new(buf)
|
91
|
+
br.read_4b # Signature
|
92
|
+
br.read_2b # Version needed to extract
|
93
|
+
expect(br.read_2b).to eq(2048) # gp flags
|
94
|
+
br.read_2b
|
95
|
+
br.read_2b
|
96
|
+
br.read_2b
|
97
|
+
br.read_4b
|
98
|
+
br.read_4b
|
99
|
+
br.read_4b
|
100
|
+
br.read_2b
|
101
|
+
br.read_2b
|
102
|
+
filename_readback = br.read_n('Kungälv'.bytesize)
|
103
|
+
expect(filename_readback.force_encoding(Encoding::UTF_8)).to eq('Kungälv')
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'writes out the local file header for an entry that requires Zip64 based on its compressed size _only_' do
|
107
|
+
buf = StringIO.new
|
108
|
+
zip = described_class.new
|
109
|
+
mtime = Time.utc(2016, 7, 17, 13, 48)
|
110
|
+
zip.add_local_file_header(io: buf, filename: 'first-file.bin', crc32: 123, compressed_size: (0xFFFFFFFF + 1),
|
111
|
+
uncompressed_size: 90981, storage_mode: 8, mtime: mtime)
|
112
|
+
|
113
|
+
buf.rewind
|
114
|
+
br = ByteReader.new(buf)
|
115
|
+
expect(br.read_4b).to eq(0x04034b50) # Signature
|
116
|
+
expect(br.read_2b).to eq(45) # Version needed to extract (require Zip64 support)
|
117
|
+
expect(br.read_2b).to eq(0) # gp flags
|
118
|
+
expect(br.read_2b).to eq(8) # storage mode
|
119
|
+
expect(br.read_2b).to eq(28160) # DOS time
|
120
|
+
expect(br.read_2b).to eq(18673) # DOS date
|
121
|
+
expect(br.read_4b).to eq(123) # CRC32
|
122
|
+
expect(br.read_4b).to eq(0xFFFFFFFF) # compressed size (blanked out)
|
123
|
+
expect(br.read_4b).to eq(0xFFFFFFFF) # uncompressed size (blanked out)
|
124
|
+
expect(br.read_2b).to eq('first-file.bin'.bytesize) # byte length of the filename
|
125
|
+
expect(br.read_2b).to eq(20) # size of extra fields
|
126
|
+
expect(br.read_n('first-file.bin'.bytesize)).to eq('first-file.bin') # the filename
|
127
|
+
expect(br.read_2b).to eq(1) # Zip64 extra field signature
|
128
|
+
expect(br.read_2b).to eq(16) # Size of the Zip64 extra field
|
129
|
+
expect(br.read_8b).to eq(90981) # True compressed size
|
130
|
+
expect(br.read_8b).to eq(0xFFFFFFFF + 1) # True uncompressed size
|
131
|
+
expect(buf).to be_eof
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'writes out the local file header for an entry that requires Zip64 based on its uncompressed size _only_' do
|
135
|
+
buf = StringIO.new
|
136
|
+
zip = described_class.new
|
137
|
+
mtime = Time.utc(2016, 7, 17, 13, 48)
|
138
|
+
zip.add_local_file_header(io: buf, filename: 'first-file.bin', crc32: 123, compressed_size: 90981,
|
139
|
+
uncompressed_size: (0xFFFFFFFF + 1), storage_mode: 8, mtime: mtime)
|
140
|
+
|
141
|
+
buf.rewind
|
142
|
+
br = ByteReader.new(buf)
|
143
|
+
expect(br.read_4b).to eq(0x04034b50) # Signature
|
144
|
+
expect(br.read_2b).to eq(45) # Version needed to extract (require Zip64 support)
|
145
|
+
expect(br.read_2b).to eq(0) # gp flags
|
146
|
+
expect(br.read_2b).to eq(8) # storage mode
|
147
|
+
expect(br.read_2b).to eq(28160) # DOS time
|
148
|
+
expect(br.read_2b).to eq(18673) # DOS date
|
149
|
+
expect(br.read_4b).to eq(123) # CRC32
|
150
|
+
expect(br.read_4b).to eq(0xFFFFFFFF) # compressed size (blanked out)
|
151
|
+
expect(br.read_4b).to eq(0xFFFFFFFF) # uncompressed size (blanked out)
|
152
|
+
expect(br.read_2b).to eq('first-file.bin'.bytesize) # byte length of the filename
|
153
|
+
expect(br.read_2b).to eq(20) # size of extra fields
|
154
|
+
expect(br.read_n('first-file.bin'.bytesize)).to eq('first-file.bin') # the filename
|
155
|
+
expect(br.read_2b).to eq(1) # Zip64 extra field signature
|
156
|
+
expect(br.read_2b).to eq(16) # Size of the Zip64 extra field
|
157
|
+
expect(br.read_8b).to eq(0xFFFFFFFF + 1) # True uncompressed size
|
158
|
+
expect(br.read_8b).to eq(90981) # True compressed size
|
159
|
+
expect(buf).to be_eof
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'does not write out the Zip64 extra if the position in the destination IO is beyond the Zip64 size limit' do
|
163
|
+
buf = StringIO.new
|
164
|
+
zip = described_class.new
|
165
|
+
mtime = Time.utc(2016, 7, 17, 13, 48)
|
166
|
+
expect(buf).to receive(:tell).and_return(0xFFFFFFFF + 1)
|
167
|
+
zip.add_local_file_header(io: buf, filename: 'first-file.bin', crc32: 123, compressed_size: 123,
|
168
|
+
uncompressed_size: 456, storage_mode: 8, mtime: mtime)
|
169
|
+
|
170
|
+
buf.rewind
|
171
|
+
br = ByteReader.new(buf)
|
172
|
+
expect(br.read_4b).to eq(0x04034b50) # Signature
|
173
|
+
expect(br.read_2b).to eq(20) # Version needed to extract (require Zip64 support)
|
174
|
+
br.read_2b
|
175
|
+
br.read_2b
|
176
|
+
br.read_2b
|
177
|
+
br.read_2b
|
178
|
+
br.read_4b
|
179
|
+
br.read_4b
|
180
|
+
br.read_4b
|
181
|
+
br.read_2b
|
182
|
+
expect(br.read_2b).to be_zero
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe '#write_central_directory' do
|
187
|
+
it 'can write the central directory and makes it a valid one even if there were no files' do
|
188
|
+
buf = StringIO.new
|
189
|
+
|
190
|
+
zip = described_class.new
|
191
|
+
zip.write_central_directory(buf)
|
192
|
+
|
193
|
+
buf.rewind
|
194
|
+
br = ByteReader.new(buf)
|
195
|
+
expect(br.read_4b).to eq(0x06054b50) # EOCD signature
|
196
|
+
expect(br.read_2b).to eq(0) # disk number
|
197
|
+
expect(br.read_2b).to eq(0) # disk number of the disk containing EOCD
|
198
|
+
expect(br.read_2b).to eq(0) # num files in the central directory of this disk
|
199
|
+
expect(br.read_2b).to eq(0) # num files in the central directories of all disks
|
200
|
+
expect(br.read_4b).to eq(0) # central directorys size
|
201
|
+
expect(br.read_4b).to eq(0) # offset of start of central directory from the beginning of the disk
|
202
|
+
expect(br.read_2b).to eq(0) # ZIP file comment length
|
203
|
+
expect(buf).to be_eof
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'writes the central directory for 2 files' do
|
207
|
+
zip = described_class.new
|
208
|
+
|
209
|
+
mtime = Time.utc(2016, 7, 17, 13, 48)
|
210
|
+
|
211
|
+
buf = StringIO.new
|
212
|
+
zip.add_local_file_header(io: buf, filename: 'first-file.bin', crc32: 123, compressed_size: 5,
|
213
|
+
uncompressed_size: 8, storage_mode: 8, mtime: mtime)
|
214
|
+
buf << Random.new.bytes(5)
|
215
|
+
zip.add_local_file_header(io: buf, filename: 'first-file.txt', crc32: 123, compressed_size: 9,
|
216
|
+
uncompressed_size: 9, storage_mode: 0, mtime: mtime)
|
217
|
+
buf << Random.new.bytes(5)
|
218
|
+
|
219
|
+
central_dir_offset = buf.tell
|
220
|
+
|
221
|
+
zip.write_central_directory(buf)
|
222
|
+
|
223
|
+
# Seek to where the central directory begins
|
224
|
+
buf.rewind
|
225
|
+
buf.seek(central_dir_offset)
|
226
|
+
|
227
|
+
br = ByteReader.new(buf)
|
228
|
+
expect(br.read_4b).to eq(0x02014b50) # Central directory entry sig
|
229
|
+
|
230
|
+
skip "Not finished"
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'writes the central directory 1 file that is larger than 4GB'
|
234
|
+
it 'writes the central directory for 2 files which, together, make the central directory start beyound the 4GB threshold'
|
235
|
+
end
|
236
|
+
end
|
@@ -13,10 +13,10 @@ describe ZipTricks::StoredSizeEstimator do
|
|
13
13
|
|
14
14
|
estimator.add_stored_entry("second-file.bin", raw_file_2.size)
|
15
15
|
|
16
|
-
r = estimator.add_compressed_entry("second-
|
16
|
+
r = estimator.add_compressed_entry("second-flie.bin", raw_file_2.size, raw_file_3.size)
|
17
17
|
expect(r).to eq(estimator), "add_compressed_entry should return self"
|
18
18
|
end
|
19
19
|
|
20
|
-
expect(predicted_size).to eq(
|
20
|
+
expect(predicted_size).to eq(1410585)
|
21
21
|
end
|
22
22
|
end
|
@@ -38,14 +38,14 @@ describe ZipTricks::Streamer do
|
|
38
38
|
expect(retval).to eq(zip)
|
39
39
|
expect(io.tell).to eq(8950)
|
40
40
|
|
41
|
-
pos = zip.add_stored_entry('
|
41
|
+
pos = zip.add_stored_entry('filf.jpg', 8921, 182919)
|
42
42
|
expect(pos).to eq(8988)
|
43
43
|
zip << SecureRandom.random_bytes(8921)
|
44
44
|
expect(io.tell).to eq(17909)
|
45
45
|
|
46
46
|
pos = zip.write_central_directory!
|
47
47
|
expect(pos).to eq(io.tell)
|
48
|
-
expect(pos).to eq(
|
48
|
+
expect(pos).to eq(18039)
|
49
49
|
|
50
50
|
pos_after_close = zip.close
|
51
51
|
expect(pos_after_close).to eq(pos)
|
@@ -90,17 +90,10 @@ describe ZipTricks::Streamer do
|
|
90
90
|
expect(per_filename['compressed-file.bin'].bytesize).to eq(f.size)
|
91
91
|
expect(Digest::SHA1.hexdigest(per_filename['compressed-file.bin'])).to eq(Digest::SHA1.hexdigest(f.read))
|
92
92
|
|
93
|
-
|
94
|
-
puts output.inspect
|
93
|
+
inspect_zip_with_external_tool(zip_file.path)
|
95
94
|
end
|
96
95
|
|
97
|
-
|
98
96
|
it 'creates an archive that OSX ArchiveUtility can handle' do
|
99
|
-
au_path = '/System/Library/CoreServices/Applications/Archive Utility.app/Contents/MacOS/Archive Utility'
|
100
|
-
unless File.exist?(au_path)
|
101
|
-
skip "This system does not have ArchiveUtility"
|
102
|
-
end
|
103
|
-
|
104
97
|
outbuf = Tempfile.new('zip')
|
105
98
|
outbuf.binmode
|
106
99
|
|
@@ -131,13 +124,10 @@ describe ZipTricks::Streamer do
|
|
131
124
|
outbuf.flush
|
132
125
|
File.unlink('test.zip') rescue nil
|
133
126
|
File.rename(outbuf.path, 'osx-archive-test.zip')
|
134
|
-
|
135
|
-
#
|
136
|
-
|
137
|
-
# Visual inspection should show whether the unarchiving is handled correctly.
|
138
|
-
`#{Shellwords.join([au_path, 'osx-archive-test.zip'])}`
|
127
|
+
|
128
|
+
# Mark this test as skipped if the system does not have the binary
|
129
|
+
open_zip_with_archive_utility('osx-archive-test.zip', skip_if_missing: true)
|
139
130
|
end
|
140
|
-
|
141
131
|
FileUtils.rm_rf('osx-archive-test')
|
142
132
|
FileUtils.rm_rf('osx-archive-test.zip')
|
143
133
|
end
|
@@ -188,8 +178,7 @@ describe ZipTricks::Streamer do
|
|
188
178
|
wd = Dir.pwd
|
189
179
|
Dir.mktmpdir do | td |
|
190
180
|
Dir.chdir(td)
|
191
|
-
|
192
|
-
puts output.inspect
|
181
|
+
inspect_zip_with_external_tool(zip_buf.path)
|
193
182
|
end
|
194
183
|
Dir.chdir(wd)
|
195
184
|
end
|
data/zip_tricks.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: zip_tricks 2.
|
5
|
+
# stub: zip_tricks 2.8.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "zip_tricks"
|
9
|
-
s.version = "2.
|
9
|
+
s.version = "2.8.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Julik Tarkhanov"]
|
14
|
-
s.date = "2016-07-
|
14
|
+
s.date = "2016-07-18"
|
15
15
|
s.description = "Makes rubyzip stream, for real"
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -35,6 +35,7 @@ Gem::Specification.new do |s|
|
|
35
35
|
"lib/zip_tricks/block_deflate.rb",
|
36
36
|
"lib/zip_tricks/block_write.rb",
|
37
37
|
"lib/zip_tricks/manifest.rb",
|
38
|
+
"lib/zip_tricks/microzip.rb",
|
38
39
|
"lib/zip_tricks/null_writer.rb",
|
39
40
|
"lib/zip_tricks/rack_body.rb",
|
40
41
|
"lib/zip_tricks/remote_io.rb",
|
@@ -47,6 +48,8 @@ Gem::Specification.new do |s|
|
|
47
48
|
"spec/zip_tricks/block_deflate_spec.rb",
|
48
49
|
"spec/zip_tricks/block_write_spec.rb",
|
49
50
|
"spec/zip_tricks/manifest_spec.rb",
|
51
|
+
"spec/zip_tricks/microzip_interop_spec.rb",
|
52
|
+
"spec/zip_tricks/microzip_spec.rb",
|
50
53
|
"spec/zip_tricks/rack_body_spec.rb",
|
51
54
|
"spec/zip_tricks/remote_io_spec.rb",
|
52
55
|
"spec/zip_tricks/remote_uncap_spec.rb",
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zip_tricks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-07-
|
11
|
+
date: 2016-07-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|
@@ -172,6 +172,7 @@ files:
|
|
172
172
|
- lib/zip_tricks/block_deflate.rb
|
173
173
|
- lib/zip_tricks/block_write.rb
|
174
174
|
- lib/zip_tricks/manifest.rb
|
175
|
+
- lib/zip_tricks/microzip.rb
|
175
176
|
- lib/zip_tricks/null_writer.rb
|
176
177
|
- lib/zip_tricks/rack_body.rb
|
177
178
|
- lib/zip_tricks/remote_io.rb
|
@@ -184,6 +185,8 @@ files:
|
|
184
185
|
- spec/zip_tricks/block_deflate_spec.rb
|
185
186
|
- spec/zip_tricks/block_write_spec.rb
|
186
187
|
- spec/zip_tricks/manifest_spec.rb
|
188
|
+
- spec/zip_tricks/microzip_interop_spec.rb
|
189
|
+
- spec/zip_tricks/microzip_spec.rb
|
187
190
|
- spec/zip_tricks/rack_body_spec.rb
|
188
191
|
- spec/zip_tricks/remote_io_spec.rb
|
189
192
|
- spec/zip_tricks/remote_uncap_spec.rb
|