zip_tricks 2.7.0 → 2.8.0
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/.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
|