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 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