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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5cd89ab487709ccaf797ab39fa9507ee0ac793e1
4
- data.tar.gz: 8c21a1106a9398dca0d4bb2e32bcb4a9f9b8b1ac
3
+ metadata.gz: 84302e70bc873418b8a458456a75e0da7b5bc67d
4
+ data.tar.gz: 06f81fc860d5cf77fdbd4b570602ae4984297d43
5
5
  SHA512:
6
- metadata.gz: d1bf6cfe7579dc8d0e21c625b135b06dcc036acb148ed197a0f1657f673bf5a77ce826a6df445b902a6dd556287c68cce7ce9aca080fe735cb405ceb5ac8f2bc
7
- data.tar.gz: 9717bf12a2559c743b2855ce9ab78279968ff12c1b6036df07fb7db0d1b052955f3bd7b6e9faec6d608a84cbbe08e26eb81fc817ea2090d516a3ea83f9632811
6
+ metadata.gz: 932fe6e3a095f43996505c642fce11009efba4298302e958f5fdf7649cd3ca9fd28ab1de6a33d1e20be2955a7efd58b4d432486d3004ab70437a55413c27934d
7
+ data.tar.gz: b9d43dbf16156cd3c877a8353dd0dd4a34e4c8b2e43664e34c4c3f2562c3a9937390bf398b0278449bf338f89337c348f30da637414e61b17eab58a939b05fc5
data/.travis.yml CHANGED
@@ -6,4 +6,5 @@ sudo: false
6
6
  cache: bundler
7
7
  matrix:
8
8
  allow_failures:
9
- - rvm: jruby-9.0
9
+ - rvm: jruby-9.0
10
+ script: bundle exec rspec
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
@@ -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
- entry = ::Zip::Entry.new(@file_name, entry_name)
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
- entry = ::Zip::Entry.new(@file_name, entry_name)
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
- cdir = Zip::CentralDirectory.new(@entry_set, comment = nil)
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
@@ -2,7 +2,7 @@ require 'zip'
2
2
  require 'very_tiny_state_machine'
3
3
 
4
4
  module ZipTricks
5
- VERSION = '2.7.0'
5
+ VERSION = '2.8.0'
6
6
 
7
7
  # Require all the sub-components except myself
8
8
  Dir.glob(__dir__ + '/**/*.rb').sort.each {|p| require p unless p == __FILE__ }
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
- # Requires supporting files with custom matchers and macros, etc,
9
- # in ./support/ and its subdirectories.
10
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
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
@@ -80,7 +80,9 @@ describe ZipTricks::RemoteIO do
80
80
  end
81
81
 
82
82
  after :each do
83
- @buf.close; @buf.unlink
83
+ if @buf
84
+ @buf.close; @buf.unlink
85
+ end
84
86
  end
85
87
 
86
88
  context 'without arguments' do
@@ -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-file.bin", raw_file_2.size, raw_file_3.size)
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(1410524)
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('file.jpg', 8921, 182919)
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(17985)
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
- output = `unzip -v #{zip_file.path}`
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
- # ArchiveUtility sometimes puts the stuff it unarchives in ~/Downloads etc. so do
136
- # not perform any checks on the files since we do not really know where they are on disk.
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
- output = `unzip -v #{zip_buf.path}`
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.7.0 ruby lib
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.7.0"
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-15"
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.7.0
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-15 00:00:00.000000000 Z
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