zip_tricks 2.8.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -3
  3. data/IMPLEMENTATION_DETAILS.md +2 -10
  4. data/README.md +62 -59
  5. data/examples/archive_size_estimate.rb +4 -4
  6. data/examples/rack_application.rb +3 -5
  7. data/lib/zip_tricks/block_deflate.rb +21 -0
  8. data/lib/zip_tricks/file_reader.rb +491 -0
  9. data/lib/zip_tricks/null_writer.rb +7 -2
  10. data/lib/zip_tricks/rack_body.rb +3 -3
  11. data/lib/zip_tricks/remote_io.rb +30 -20
  12. data/lib/zip_tricks/remote_uncap.rb +10 -10
  13. data/lib/zip_tricks/size_estimator.rb +64 -0
  14. data/lib/zip_tricks/stream_crc32.rb +2 -2
  15. data/lib/zip_tricks/streamer/deflated_writer.rb +26 -0
  16. data/lib/zip_tricks/streamer/entry.rb +21 -0
  17. data/lib/zip_tricks/streamer/stored_writer.rb +25 -0
  18. data/lib/zip_tricks/streamer/writable.rb +20 -0
  19. data/lib/zip_tricks/streamer.rb +172 -66
  20. data/lib/zip_tricks/zip_writer.rb +346 -0
  21. data/lib/zip_tricks.rb +1 -4
  22. data/spec/spec_helper.rb +1 -38
  23. data/spec/zip_tricks/file_reader_spec.rb +47 -0
  24. data/spec/zip_tricks/rack_body_spec.rb +2 -2
  25. data/spec/zip_tricks/remote_io_spec.rb +8 -20
  26. data/spec/zip_tricks/remote_uncap_spec.rb +4 -4
  27. data/spec/zip_tricks/size_estimator_spec.rb +31 -0
  28. data/spec/zip_tricks/streamer_spec.rb +59 -36
  29. data/spec/zip_tricks/zip_writer_spec.rb +408 -0
  30. data/zip_tricks.gemspec +20 -14
  31. metadata +33 -16
  32. data/lib/zip_tricks/manifest.rb +0 -85
  33. data/lib/zip_tricks/microzip.rb +0 -339
  34. data/lib/zip_tricks/stored_size_estimator.rb +0 -44
  35. data/spec/zip_tricks/manifest_spec.rb +0 -60
  36. data/spec/zip_tricks/microzip_interop_spec.rb +0 -48
  37. data/spec/zip_tricks/microzip_spec.rb +0 -546
  38. data/spec/zip_tricks/stored_size_estimator_spec.rb +0 -22
@@ -1,6 +1,4 @@
1
1
  require_relative '../spec_helper'
2
- require 'fileutils'
3
- require 'shellwords'
4
2
 
5
3
  describe ZipTricks::Streamer do
6
4
  let(:test_text_file_path) {
@@ -30,7 +28,7 @@ describe ZipTricks::Streamer do
30
28
  it 'returns the position in the IO at every call' do
31
29
  io = StringIO.new
32
30
  zip = described_class.new(io)
33
- pos = zip.add_compressed_entry('file.jpg', 182919, 8921, 8912)
31
+ pos = zip.add_compressed_entry(filename: 'file.jpg', uncompressed_size: 182919, compressed_size: 8912, crc32: 8912)
34
32
  expect(pos).to eq(io.tell)
35
33
  expect(pos).to eq(38)
36
34
 
@@ -38,17 +36,14 @@ describe ZipTricks::Streamer do
38
36
  expect(retval).to eq(zip)
39
37
  expect(io.tell).to eq(8950)
40
38
 
41
- pos = zip.add_stored_entry('filf.jpg', 8921, 182919)
39
+ pos = zip.add_stored_entry(filename: 'filf.jpg', size: 8921, crc32: 182919)
42
40
  expect(pos).to eq(8988)
43
41
  zip << SecureRandom.random_bytes(8921)
44
42
  expect(io.tell).to eq(17909)
45
43
 
46
- pos = zip.write_central_directory!
44
+ pos = zip.close
47
45
  expect(pos).to eq(io.tell)
48
- expect(pos).to eq(18039)
49
-
50
- pos_after_close = zip.close
51
- expect(pos_after_close).to eq(pos)
46
+ expect(pos).to eq(18068)
52
47
  end
53
48
 
54
49
  it 'can write and then read the block-deflated files' do
@@ -73,7 +68,8 @@ describe ZipTricks::Streamer do
73
68
  zip_file.binmode
74
69
 
75
70
  described_class.open(zip_file) do |zip|
76
- zip.add_compressed_entry("compressed-file.bin", f.size, crc, compressed_blockwise.size)
71
+ zip.add_compressed_entry(filename: "compressed-file.bin", uncompressed_size: f.size,
72
+ crc32: crc, compressed_size: compressed_blockwise.size)
77
73
  zip << compressed_blockwise.read
78
74
  end
79
75
  zip_file.flush
@@ -112,11 +108,12 @@ describe ZipTricks::Streamer do
112
108
  end
113
109
 
114
110
  # Add this file compressed...
115
- zip.add_compressed_entry('war-and-peace.txt', source_f.size, crc32, compressed_buffer.size)
111
+ zip.add_compressed_entry(filename: 'war-and-peace.txt', uncompressed_size: source_f.size,
112
+ crc32: crc32, compressed_size: compressed_buffer.size)
116
113
  zip << compressed_buffer.string
117
114
 
118
115
  # ...and stored.
119
- zip.add_stored_entry('war-and-peace-raw.txt', source_f.size, crc32)
116
+ zip.add_stored_entry(filename: 'war-and-peace-raw.txt', size: source_f.size, crc32: crc32)
120
117
  zip << source_f.read
121
118
 
122
119
  zip.close
@@ -154,9 +151,9 @@ describe ZipTricks::Streamer do
154
151
 
155
152
  # Perform the zipping
156
153
  zip = described_class.new(output_io)
157
- zip.add_stored_entry("first-file.bin", raw_file_1.size, Zlib.crc32(raw_file_1))
154
+ zip.add_stored_entry(filename: "first-file.bin", size: raw_file_1.size, crc32: Zlib.crc32(raw_file_1))
158
155
  zip << raw_file_1
159
- zip.add_stored_entry("second-file.bin", raw_file_2.size, Zlib.crc32(raw_file_2))
156
+ zip.add_stored_entry(filename: "second-file.bin", size: raw_file_2.size, crc32: Zlib.crc32(raw_file_2))
160
157
  zip << raw_file_2
161
158
  zip.close
162
159
 
@@ -193,9 +190,9 @@ describe ZipTricks::Streamer do
193
190
 
194
191
  # Perform the zipping
195
192
  zip = described_class.new(zip_buf)
196
- zip.add_stored_entry("first-file.bin", raw_file_1.size, Zlib.crc32(raw_file_1))
193
+ zip.add_stored_entry(filename: "first-file.bin", size: raw_file_1.size, crc32: Zlib.crc32(raw_file_1))
197
194
  zip << raw_file_1
198
- zip.add_stored_entry("второй-файл.bin", raw_file_2.size, Zlib.crc32(raw_file_2))
195
+ zip.add_stored_entry(filename: "второй-файл.bin", size: raw_file_2.size, crc32: Zlib.crc32(raw_file_2))
199
196
  IO.copy_stream(StringIO.new(raw_file_2), zip)
200
197
  zip.close
201
198
 
@@ -215,28 +212,54 @@ describe ZipTricks::Streamer do
215
212
  expect(second_entry.name).to eq("второй-файл.bin".force_encoding(Encoding::BINARY))
216
213
  end
217
214
  end
218
-
219
- it 'raises when the actual bytes written for a stored entry does not match the entry header' do
220
- expect {
221
- ZipTricks::Streamer.open(StringIO.new) do | zip |
222
- zip.add_stored_entry('file', 123, 0)
223
- zip << 'xx'
215
+
216
+ it 'creates an archive with data descriptors that can be opened by Rubyzip, with a small number of very tiny text files' do
217
+ tf = ManagedTempfile.new('zip')
218
+ z = described_class.open(tf) do |zip|
219
+ zip.write_stored_file('deflated.txt') do |sink|
220
+ sink << File.read(__dir__ + '/war-and-peace.txt')
224
221
  end
225
- }.to raise_error {|e|
226
- expect(e).to be_kind_of(ZipTricks::Streamer::EntryBodySizeMismatch)
227
- expect(e.message).to eq('Wrong number of bytes written for entry (expected 123, got 2)')
228
- }
222
+ zip.write_deflated_file('stored.txt') do |sink|
223
+ sink << File.read(__dir__ + '/war-and-peace.txt')
224
+ end
225
+ end
226
+ tf.flush
227
+
228
+ pending 'https://github.com/rubyzip/rubyzip/issues/295'
229
+
230
+ Zip::File.foreach(tf.path) do |entry|
231
+ # Make sure it is tagged as UNIX
232
+ expect(entry.fstype).to eq(3)
233
+
234
+ # The CRC
235
+ expect(entry.crc).to eq(Zlib.crc32(File.read(__dir__ + '/war-and-peace.txt')))
236
+
237
+ # Check the name
238
+ expect(entry.name).to match(/\.txt$/)
239
+
240
+ # Check the right external attributes (non-executable on UNIX)
241
+ expect(entry.external_file_attributes).to eq(2175008768)
242
+
243
+ # Check the file contents
244
+ readback = entry.get_input_stream.read
245
+ readback.force_encoding(Encoding::BINARY)
246
+ expect(readback[0..10]).to eq(File.read(__dir__ + '/war-and-peace.txt')[0..10])
247
+ end
248
+
249
+ inspect_zip_with_external_tool(tf.path)
229
250
  end
230
251
 
231
- it 'raises when the actual bytes written for a compressed entry does not match the entry header' do
232
- expect {
233
- ZipTricks::Streamer.open(StringIO.new) do | zip |
234
- zip.add_compressed_entry('file', 1898121, 0, 123)
235
- zip << 'xx'
236
- end
237
- }.to raise_error {|e|
238
- expect(e).to be_kind_of(ZipTricks::Streamer::EntryBodySizeMismatch)
239
- expect(e.message).to eq('Wrong number of bytes written for entry (expected 123, got 2)')
240
- }
252
+ it 'can create a valid ZIP archive without any files' do
253
+ tf = ManagedTempfile.new('zip')
254
+
255
+ described_class.open(tf) do |zip|
256
+ end
257
+
258
+ tf.flush
259
+ tf.rewind
260
+
261
+ expect { |b|
262
+ Zip::File.foreach(tf.path, &b)
263
+ }.not_to yield_control
241
264
  end
242
265
  end
@@ -0,0 +1,408 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../testing/support'
3
+
4
+ describe ZipTricks::ZipWriter do
5
+ class ByteReader < Struct.new(:io)
6
+ def initialize(io)
7
+ super(io).tap { io.rewind }
8
+ end
9
+
10
+ def read_2b
11
+ read_n(2).unpack('v').first
12
+ end
13
+
14
+ def read_2c
15
+ read_n(2).unpack('CC').first
16
+ end
17
+
18
+ def read_4b
19
+ read_n(4).unpack('V').first
20
+ end
21
+
22
+ def read_8b
23
+ read_n(8).unpack('Q<').first
24
+ end
25
+
26
+ def read_n(n)
27
+ io.read(n).tap {|r|
28
+ raise "Expected to read #{n} bytes, but read() returned nil" if r.nil?
29
+ raise "Expected to read #{n} bytes, but read #{r.bytesize} instead" if r.bytesize != n
30
+ }
31
+ end
32
+
33
+ # For conveniently going to a specific signature
34
+ def seek_to_start_of_signature(signature)
35
+ io.rewind
36
+ signature_encoded = [signature].pack('V')
37
+ idx = io.read.index(signature_encoded)
38
+ raise "Could not find the signature #{signature} in the buffer" unless idx
39
+ io.seek(idx, IO::SEEK_SET)
40
+ end
41
+ end
42
+
43
+ describe '#write_local_file_header' do
44
+ it 'writes the local file header for an entry that does not require Zip64' do
45
+ buf = StringIO.new
46
+ mtime = Time.utc(2016, 7, 17, 13, 48)
47
+
48
+ subject = ZipTricks::ZipWriter.new
49
+ subject.write_local_file_header(io: buf, gp_flags: 12, crc32: 456, compressed_size: 768, uncompressed_size: 901, mtime: mtime, filename: 'foo.bin', storage_mode: 8)
50
+
51
+ br = ByteReader.new(buf)
52
+ expect(br.read_4b).to eq(0x04034b50) # Signature
53
+ expect(br.read_2b).to eq(20) # Version needed to extract
54
+ expect(br.read_2b).to eq(12) # gp flags
55
+ expect(br.read_2b).to eq(8) # storage mode
56
+ expect(br.read_2b).to eq(28160) # DOS time
57
+ expect(br.read_2b).to eq(18673) # DOS date
58
+ expect(br.read_4b).to eq(456) # CRC32
59
+ expect(br.read_4b).to eq(768) # compressed size
60
+ expect(br.read_4b).to eq(901) # uncompressed size
61
+ expect(br.read_2b).to eq(7) # filename size
62
+ expect(br.read_2b).to eq(0) # extra fields size
63
+ expect(br.read_n(7)).to eq('foo.bin') # extra fields size
64
+ expect(buf).to be_eof
65
+ end
66
+
67
+ it 'writes the local file header for an entry that does require Zip64 based on uncompressed size (with the Zip64 extra)' do
68
+ buf = StringIO.new
69
+ mtime = Time.utc(2016, 7, 17, 13, 48)
70
+
71
+ subject = ZipTricks::ZipWriter.new
72
+ subject.write_local_file_header(io: buf, gp_flags: 12, crc32: 456, compressed_size: 768, uncompressed_size: 0xFFFFFFFF+1, mtime: mtime, filename: 'foo.bin', storage_mode: 8)
73
+
74
+ br = ByteReader.new(buf)
75
+ expect(br.read_4b).to eq(0x04034b50) # Signature
76
+ expect(br.read_2b).to eq(45) # Version needed to extract
77
+ expect(br.read_2b).to eq(12) # gp flags
78
+ expect(br.read_2b).to eq(8) # storage mode
79
+ expect(br.read_2b).to eq(28160) # DOS time
80
+ expect(br.read_2b).to eq(18673) # DOS date
81
+ expect(br.read_4b).to eq(456) # CRC32
82
+ expect(br.read_4b).to eq(0xFFFFFFFF) # compressed size
83
+ expect(br.read_4b).to eq(0xFFFFFFFF) # uncompressed size
84
+ expect(br.read_2b).to eq(7) # filename size
85
+ expect(br.read_2b).to eq(20) # extra fields size
86
+ expect(br.read_n(7)).to eq('foo.bin') # extra fields size
87
+
88
+ expect(buf).not_to be_eof
89
+
90
+ expect(br.read_2b).to eq(1) # Zip64 extra tag
91
+ expect(br.read_2b).to eq(16) # Size of the Zip64 extra payload
92
+ expect(br.read_8b).to eq(0xFFFFFFFF+1) # uncompressed size
93
+ expect(br.read_8b).to eq(768) # compressed size
94
+ end
95
+
96
+ it 'writes the local file header for an entry that does require Zip64 based on compressed size (with the Zip64 extra)' do
97
+ buf = StringIO.new
98
+ mtime = Time.utc(2016, 7, 17, 13, 48)
99
+
100
+ subject = ZipTricks::ZipWriter.new
101
+ subject.write_local_file_header(io: buf, gp_flags: 12, crc32: 456, compressed_size: 0xFFFFFFFF+1, uncompressed_size: 768, mtime: mtime, filename: 'foo.bin', storage_mode: 8)
102
+
103
+ br = ByteReader.new(buf)
104
+ expect(br.read_4b).to eq(0x04034b50) # Signature
105
+ expect(br.read_2b).to eq(45) # Version needed to extract
106
+ expect(br.read_2b).to eq(12) # gp flags
107
+ expect(br.read_2b).to eq(8) # storage mode
108
+ expect(br.read_2b).to eq(28160) # DOS time
109
+ expect(br.read_2b).to eq(18673) # DOS date
110
+ expect(br.read_4b).to eq(456) # CRC32
111
+ expect(br.read_4b).to eq(0xFFFFFFFF) # compressed size
112
+ expect(br.read_4b).to eq(0xFFFFFFFF) # uncompressed size
113
+ expect(br.read_2b).to eq(7) # filename size
114
+ expect(br.read_2b).to eq(20) # extra fields size
115
+ expect(br.read_n(7)).to eq('foo.bin') # extra fields size
116
+
117
+ expect(buf).not_to be_eof
118
+
119
+ expect(br.read_2b).to eq(1) # Zip64 extra tag
120
+ expect(br.read_2b).to eq(16) # Size of the Zip64 extra payload
121
+ expect(br.read_8b).to eq(768) # uncompressed size
122
+ expect(br.read_8b).to eq(0xFFFFFFFF+1) # compressed size
123
+ end
124
+ end
125
+
126
+ describe '#write_data_descriptor' do
127
+ it 'writes 4-byte sizes into the data descriptor for standard file sizes' do
128
+ buf = StringIO.new
129
+
130
+ subject.write_data_descriptor(io: buf, crc32: 123, compressed_size: 89821, uncompressed_size: 990912)
131
+
132
+ br = ByteReader.new(buf)
133
+ expect(br.read_4b).to eq(0x08074b50) # Signature
134
+ expect(br.read_4b).to eq(123) # CRC32
135
+ expect(br.read_4b).to eq(89821) # compressed size
136
+ expect(br.read_4b).to eq(990912) # uncompressed size
137
+ expect(buf).to be_eof
138
+ end
139
+
140
+ it 'writes 8-byte sizes into the data descriptor for Zip64 compressed file size' do
141
+ buf = StringIO.new
142
+
143
+ subject.write_data_descriptor(io: buf, crc32: 123, compressed_size: 0xFFFFFFFF + 1, uncompressed_size: 990912)
144
+
145
+ br = ByteReader.new(buf)
146
+ expect(br.read_4b).to eq(0x08074b50) # Signature
147
+ expect(br.read_4b).to eq(123) # CRC32
148
+ expect(br.read_8b).to eq(0xFFFFFFFF + 1) # compressed size
149
+ expect(br.read_8b).to eq(990912) # uncompressed size
150
+ expect(buf).to be_eof
151
+ end
152
+
153
+ it 'writes 8-byte sizes into the data descriptor for Zip64 uncompressed file size' do
154
+ buf = StringIO.new
155
+
156
+ subject.write_data_descriptor(io: buf, crc32: 123, compressed_size: 123, uncompressed_size: 0xFFFFFFFF + 1)
157
+
158
+ br = ByteReader.new(buf)
159
+ expect(br.read_4b).to eq(0x08074b50) # Signature
160
+ expect(br.read_4b).to eq(123) # CRC32
161
+ expect(br.read_8b).to eq(123) # compressed size
162
+ expect(br.read_8b).to eq(0xFFFFFFFF + 1) # uncompressed size
163
+ expect(buf).to be_eof
164
+ end
165
+ end
166
+
167
+ describe '#write_central_directory_file_header' do
168
+ it 'writes the file header for a small-ish entry' do
169
+ buf = StringIO.new
170
+
171
+ subject.write_central_directory_file_header(io: buf, local_file_header_location: 898921,
172
+ gp_flags: 555, storage_mode: 23,
173
+ compressed_size: 901, uncompressed_size: 909102,
174
+ mtime: Time.utc(2016, 2, 2, 14, 00), crc32: 89765,
175
+ filename: 'a-file.txt', external_attrs: 123)
176
+
177
+ br = ByteReader.new(buf)
178
+ expect(br.read_4b).to eq(0x02014b50) # Central directory entry sig
179
+ expect(br.read_2b).to eq(820) # version made by
180
+ expect(br.read_2b).to eq(20) # version need to extract
181
+ expect(br.read_2b).to eq(555) # general purpose bit flag (explicitly set to bogus value to ensure we pass it through)
182
+ expect(br.read_2b).to eq(23) # compression method (explicitly set to bogus value)
183
+ expect(br.read_2b).to eq(28672) # last mod file time
184
+ expect(br.read_2b).to eq(18498) # last mod file date
185
+ expect(br.read_4b).to eq(89765) # crc32
186
+ expect(br.read_4b).to eq(901) # compressed size
187
+ expect(br.read_4b).to eq(909102) # uncompressed size
188
+ expect(br.read_2b).to eq(10) # filename length
189
+ expect(br.read_2b).to eq(0) # extra field length
190
+ expect(br.read_2b).to eq(0) # file comment
191
+ expect(br.read_2b).to eq(0) # disk number, must be blanked to the maximum value because of The Unarchiver bug
192
+ expect(br.read_2b).to eq(0) # internal file attributes
193
+ expect(br.read_4b).to eq(2175008768) # external file attributes
194
+ expect(br.read_4b).to eq(898921) # relative offset of local header
195
+ expect(br.read_n(10)).to eq('a-file.txt') # the filename
196
+ end
197
+
198
+ it 'writes the file header for an entry that requires Zip64 extra because of the uncompressed size' do
199
+ buf = StringIO.new
200
+
201
+ subject.write_central_directory_file_header(io: buf, local_file_header_location: 898921,
202
+ gp_flags: 555, storage_mode: 23,
203
+ compressed_size: 901, uncompressed_size: 0xFFFFFFFFF + 3,
204
+ mtime: Time.utc(2016, 2, 2, 14, 00), crc32: 89765,
205
+ filename: 'a-file.txt', external_attrs: 123)
206
+
207
+ br = ByteReader.new(buf)
208
+ expect(br.read_4b).to eq(0x02014b50) # Central directory entry sig
209
+ expect(br.read_2b).to eq(820) # version made by
210
+ expect(br.read_2b).to eq(45) # version need to extract
211
+ expect(br.read_2b).to eq(555) # general purpose bit flag (explicitly set to bogus value to ensure we pass it through)
212
+ expect(br.read_2b).to eq(23) # compression method (explicitly set to bogus value)
213
+ expect(br.read_2b).to eq(28672) # last mod file time
214
+ expect(br.read_2b).to eq(18498) # last mod file date
215
+ expect(br.read_4b).to eq(89765) # crc32
216
+ expect(br.read_4b).to eq(0xFFFFFFFF) # compressed size
217
+ expect(br.read_4b).to eq(0xFFFFFFFF) # uncompressed size
218
+ expect(br.read_2b).to eq(10) # filename length
219
+ expect(br.read_2b).to eq(32) # extra field length
220
+ expect(br.read_2b).to eq(0) # file comment
221
+ expect(br.read_2b).to eq(0xFFFF) # disk number, must be blanked to the maximum value because of The Unarchiver bug
222
+ expect(br.read_2b).to eq(0) # internal file attributes
223
+ expect(br.read_4b).to eq(2175008768) # external file attributes
224
+ expect(br.read_4b).to eq(0xFFFFFFFF) # relative offset of local header
225
+ expect(br.read_n(10)).to eq('a-file.txt') # the filename
226
+
227
+ expect(buf).not_to be_eof
228
+ expect(br.read_2b).to eq(1) # Zip64 extra tag
229
+ expect(br.read_2b).to eq(28) # Size of the Zip64 extra payload
230
+ expect(br.read_8b).to eq(0xFFFFFFFFF + 3) # uncompressed size
231
+ expect(br.read_8b).to eq(901) # compressed size
232
+ expect(br.read_8b).to eq(898921) # local file header location
233
+ end
234
+
235
+ it 'writes the file header for an entry that requires Zip64 extra because of the compressed size' do
236
+ buf = StringIO.new
237
+
238
+ subject.write_central_directory_file_header(io: buf, local_file_header_location: 898921,
239
+ gp_flags: 555, storage_mode: 23,
240
+ compressed_size: 0xFFFFFFFFF + 3, uncompressed_size: 901, # the worst compression scheme in the universe
241
+ mtime: Time.utc(2016, 2, 2, 14, 00), crc32: 89765,
242
+ filename: 'a-file.txt', external_attrs: 123)
243
+
244
+ br = ByteReader.new(buf)
245
+ expect(br.read_4b).to eq(0x02014b50) # Central directory entry sig
246
+ expect(br.read_2b).to eq(820) # version made by
247
+ expect(br.read_2b).to eq(45) # version need to extract
248
+ expect(br.read_2b).to eq(555) # general purpose bit flag (explicitly set to bogus value to ensure we pass it through)
249
+ expect(br.read_2b).to eq(23) # compression method (explicitly set to bogus value)
250
+ expect(br.read_2b).to eq(28672) # last mod file time
251
+ expect(br.read_2b).to eq(18498) # last mod file date
252
+ expect(br.read_4b).to eq(89765) # crc32
253
+ expect(br.read_4b).to eq(0xFFFFFFFF) # compressed size
254
+ expect(br.read_4b).to eq(0xFFFFFFFF) # uncompressed size
255
+ expect(br.read_2b).to eq(10) # filename length
256
+ expect(br.read_2b).to eq(32) # extra field length
257
+ expect(br.read_2b).to eq(0) # file comment
258
+ expect(br.read_2b).to eq(0xFFFF) # disk number, must be blanked to the maximum value because of The Unarchiver bug
259
+ expect(br.read_2b).to eq(0) # internal file attributes
260
+ expect(br.read_4b).to eq(2175008768) # external file attributes
261
+ expect(br.read_4b).to eq(0xFFFFFFFF) # relative offset of local header
262
+ expect(br.read_n(10)).to eq('a-file.txt') # the filename
263
+
264
+ expect(buf).not_to be_eof
265
+ expect(br.read_2b).to eq(1) # Zip64 extra tag
266
+ expect(br.read_2b).to eq(28) # Size of the Zip64 extra payload
267
+ expect(br.read_8b).to eq(901) # uncompressed size
268
+ expect(br.read_8b).to eq(0xFFFFFFFFF + 3) # compressed size
269
+ expect(br.read_8b).to eq(898921) # local file header location
270
+ end
271
+
272
+ it 'writes the file header for an entry that requires Zip64 extra because of the local file header offset being beyound 4GB' do
273
+ buf = StringIO.new
274
+
275
+ subject.write_central_directory_file_header(io: buf, local_file_header_location: 0xFFFFFFFFF + 1,
276
+ gp_flags: 555, storage_mode: 23,
277
+ compressed_size: 8981, uncompressed_size: 819891, # the worst compression scheme in the universe
278
+ mtime: Time.utc(2016, 2, 2, 14, 00), crc32: 89765,
279
+ filename: 'a-file.txt', external_attrs: 123)
280
+
281
+ br = ByteReader.new(buf)
282
+ expect(br.read_4b).to eq(0x02014b50) # Central directory entry sig
283
+ expect(br.read_2b).to eq(820) # version made by
284
+ expect(br.read_2b).to eq(45) # version need to extract
285
+ expect(br.read_2b).to eq(555) # general purpose bit flag (explicitly set to bogus value to ensure we pass it through)
286
+ expect(br.read_2b).to eq(23) # compression method (explicitly set to bogus value)
287
+ expect(br.read_2b).to eq(28672) # last mod file time
288
+ expect(br.read_2b).to eq(18498) # last mod file date
289
+ expect(br.read_4b).to eq(89765) # crc32
290
+ expect(br.read_4b).to eq(0xFFFFFFFF) # compressed size
291
+ expect(br.read_4b).to eq(0xFFFFFFFF) # uncompressed size
292
+ expect(br.read_2b).to eq(10) # filename length
293
+ expect(br.read_2b).to eq(32) # extra field length
294
+ expect(br.read_2b).to eq(0) # file comment
295
+ expect(br.read_2b).to eq(0xFFFF) # disk number, must be blanked to the maximum value because of The Unarchiver bug
296
+ expect(br.read_2b).to eq(0) # internal file attributes
297
+ expect(br.read_4b).to eq(2175008768) # external file attributes
298
+ expect(br.read_4b).to eq(0xFFFFFFFF) # relative offset of local header
299
+ expect(br.read_n(10)).to eq('a-file.txt') # the filename
300
+
301
+ expect(buf).not_to be_eof
302
+ expect(br.read_2b).to eq(1) # Zip64 extra tag
303
+ expect(br.read_2b).to eq(28) # Size of the Zip64 extra payload
304
+ expect(br.read_8b).to eq(819891) # uncompressed size
305
+ expect(br.read_8b).to eq(8981) # compressed size
306
+ expect(br.read_8b).to eq(0xFFFFFFFFF + 1) # local file header location
307
+ end
308
+ end
309
+
310
+ describe '#write_end_of_central_directory' do
311
+ it 'writes out the EOCD with all markers for a small ZIP file with just a few entries' do
312
+ buf = StringIO.new
313
+
314
+ num_files = rand(8..190)
315
+ subject.write_end_of_central_directory(io: buf, start_of_central_directory_location: 9091211,
316
+ central_directory_size: 9091, num_files_in_archive: num_files)
317
+
318
+ br = ByteReader.new(buf)
319
+ expect(br.read_4b).to eq(0x06054b50) # EOCD signature
320
+ expect(br.read_2b).to eq(0) # number of this disk
321
+ expect(br.read_2b).to eq(0) # number of the disk with the EOCD record
322
+ expect(br.read_2b).to eq(num_files) # number of files on this disk
323
+ expect(br.read_2b).to eq(num_files) # number of files in central directory total (for all disks)
324
+ expect(br.read_4b).to eq(9091) # size of the central directory (cdir records for all files)
325
+ expect(br.read_4b).to eq(9091211) # start of central directory offset from the beginning of file/disk
326
+
327
+ comment_length = br.read_2b
328
+ expect(comment_length).not_to be_zero
329
+
330
+ expect(br.read_n(comment_length)).to match(/ZipTricks/)
331
+ end
332
+
333
+ it 'writes out the Zip64 EOCD as well if the central directory is located beyound 4GB in the archive' do
334
+ buf = StringIO.new
335
+
336
+ num_files = rand(8..190)
337
+ subject.write_end_of_central_directory(io: buf, start_of_central_directory_location: 0xFFFFFFFF + 3,
338
+ central_directory_size: 9091, num_files_in_archive: num_files)
339
+
340
+ br = ByteReader.new(buf)
341
+
342
+ expect(br.read_4b).to eq(0x06064b50) # Zip64 EOCD signature
343
+ expect(br.read_8b).to eq(44) # Zip64 EOCD record size
344
+ expect(br.read_2b).to eq(820) # Version made by
345
+ expect(br.read_2b).to eq(45) # Version needed to extract
346
+ expect(br.read_4b).to eq(0) # Number of this disk
347
+ expect(br.read_4b).to eq(0) # Number of the disk with the Zip64 EOCD record
348
+ expect(br.read_8b).to eq(num_files) # Number of entries in the central directory of this disk
349
+ expect(br.read_8b).to eq(num_files) # Number of entries in the central directories of all disks
350
+ expect(br.read_8b).to eq(9091) # Central directory size
351
+ expect(br.read_8b).to eq(0xFFFFFFFF + 3) # Start of central directory location
352
+
353
+ expect(br.read_4b).to eq(0x07064b50) # Zip64 EOCD locator signature
354
+ expect(br.read_4b).to eq(0) # Number of the disk with the EOCD locator signature
355
+ expect(br.read_8b).to eq((0xFFFFFFFF + 3) + 9091) # Where the Zip64 EOCD record starts
356
+ expect(br.read_4b).to eq(1) # Total number of disks
357
+
358
+ # Then the usual EOCD record
359
+ expect(br.read_4b).to eq(0x06054b50) # EOCD signature
360
+ expect(br.read_2b).to eq(0) # number of this disk
361
+ expect(br.read_2b).to eq(0) # number of the disk with the EOCD record
362
+ expect(br.read_2b).to eq(0xFFFF) # number of files on this disk
363
+ expect(br.read_2b).to eq(0xFFFF) # number of files in central directory total (for all disks)
364
+ expect(br.read_4b).to eq(0xFFFFFFFF) # size of the central directory (cdir records for all files)
365
+ expect(br.read_4b).to eq(0xFFFFFFFF) # start of central directory offset from the beginning of file/disk
366
+
367
+ comment_length = br.read_2b
368
+ expect(comment_length).not_to be_zero
369
+ expect(br.read_n(comment_length)).to match(/ZipTricks/)
370
+ end
371
+
372
+ it 'writes out the Zip64 EOCD if the archive has more than 0xFFFF files' do
373
+ buf = StringIO.new
374
+
375
+ subject.write_end_of_central_directory(io: buf, start_of_central_directory_location: 123,
376
+ central_directory_size: 9091, num_files_in_archive: 0xFFFF + 1)
377
+
378
+ br = ByteReader.new(buf)
379
+
380
+ expect(br.read_4b).to eq(0x06064b50) # Zip64 EOCD signature
381
+ br.read_8b
382
+ br.read_2b
383
+ br.read_2b
384
+ br.read_4b
385
+ br.read_4b
386
+ expect(br.read_8b).to eq(0xFFFF + 1) # Number of entries in the central directory of this disk
387
+ expect(br.read_8b).to eq(0xFFFF + 1) # Number of entries in the central directories of all disks
388
+ end
389
+
390
+ it 'writes out the Zip64 EOCD if the central directory size exceeds 0xFFFFFFFF' do
391
+ buf = StringIO.new
392
+
393
+ subject.write_end_of_central_directory(io: buf, start_of_central_directory_location: 123,
394
+ central_directory_size: 0xFFFFFFFF + 2, num_files_in_archive: 5)
395
+
396
+ br = ByteReader.new(buf)
397
+
398
+ expect(br.read_4b).to eq(0x06064b50) # Zip64 EOCD signature
399
+ br.read_8b
400
+ br.read_2b
401
+ br.read_2b
402
+ br.read_4b
403
+ br.read_4b
404
+ expect(br.read_8b).to eq(5) # Number of entries in the central directory of this disk
405
+ expect(br.read_8b).to eq(5) # Number of entries in the central directories of all disks
406
+ end
407
+ end
408
+ 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.8.1 ruby lib
5
+ # stub: zip_tricks 3.0.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "zip_tricks"
9
- s.version = "2.8.1"
9
+ s.version = "3.0.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-22"
14
+ s.date = "2016-08-03"
15
15
  s.description = "Makes rubyzip stream, for real"
16
16
  s.email = "me@julik.nl"
17
17
  s.extra_rdoc_files = [
@@ -35,30 +35,33 @@ Gem::Specification.new do |s|
35
35
  "lib/zip_tricks.rb",
36
36
  "lib/zip_tricks/block_deflate.rb",
37
37
  "lib/zip_tricks/block_write.rb",
38
- "lib/zip_tricks/manifest.rb",
39
- "lib/zip_tricks/microzip.rb",
38
+ "lib/zip_tricks/file_reader.rb",
40
39
  "lib/zip_tricks/null_writer.rb",
41
40
  "lib/zip_tricks/rack_body.rb",
42
41
  "lib/zip_tricks/remote_io.rb",
43
42
  "lib/zip_tricks/remote_uncap.rb",
44
- "lib/zip_tricks/stored_size_estimator.rb",
43
+ "lib/zip_tricks/size_estimator.rb",
45
44
  "lib/zip_tricks/stream_crc32.rb",
46
45
  "lib/zip_tricks/streamer.rb",
46
+ "lib/zip_tricks/streamer/deflated_writer.rb",
47
+ "lib/zip_tricks/streamer/entry.rb",
48
+ "lib/zip_tricks/streamer/stored_writer.rb",
49
+ "lib/zip_tricks/streamer/writable.rb",
47
50
  "lib/zip_tricks/write_and_tell.rb",
51
+ "lib/zip_tricks/zip_writer.rb",
48
52
  "spec/spec_helper.rb",
49
53
  "spec/zip_tricks/block_deflate_spec.rb",
50
54
  "spec/zip_tricks/block_write_spec.rb",
51
- "spec/zip_tricks/manifest_spec.rb",
52
- "spec/zip_tricks/microzip_interop_spec.rb",
53
- "spec/zip_tricks/microzip_spec.rb",
55
+ "spec/zip_tricks/file_reader_spec.rb",
54
56
  "spec/zip_tricks/rack_body_spec.rb",
55
57
  "spec/zip_tricks/remote_io_spec.rb",
56
58
  "spec/zip_tricks/remote_uncap_spec.rb",
57
- "spec/zip_tricks/stored_size_estimator_spec.rb",
59
+ "spec/zip_tricks/size_estimator_spec.rb",
58
60
  "spec/zip_tricks/stream_crc32_spec.rb",
59
61
  "spec/zip_tricks/streamer_spec.rb",
60
62
  "spec/zip_tricks/war-and-peace.txt",
61
63
  "spec/zip_tricks/write_and_tell_spec.rb",
64
+ "spec/zip_tricks/zip_writer_spec.rb",
62
65
  "zip_tricks.gemspec"
63
66
  ]
64
67
  s.homepage = "http://github.com/wetransfer/zip_tricks"
@@ -70,33 +73,36 @@ Gem::Specification.new do |s|
70
73
  s.specification_version = 4
71
74
 
72
75
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
73
- s.add_runtime_dependency(%q<rubyzip>, [">= 1.1.7", "~> 1.1"])
74
- s.add_runtime_dependency(%q<very_tiny_state_machine>, ["~> 2"])
76
+ s.add_development_dependency(%q<rubyzip>, [">= 1.1.7", "~> 1.1"])
77
+ s.add_development_dependency(%q<terminal-table>, [">= 0"])
75
78
  s.add_development_dependency(%q<range_utils>, [">= 0"])
76
79
  s.add_development_dependency(%q<rack>, ["~> 1.6"])
77
80
  s.add_development_dependency(%q<rake>, ["~> 10.4"])
78
81
  s.add_development_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
82
+ s.add_development_dependency(%q<coderay>, [">= 0"])
79
83
  s.add_development_dependency(%q<yard>, ["~> 0.8"])
80
84
  s.add_development_dependency(%q<bundler>, ["~> 1.0"])
81
85
  s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
82
86
  else
83
87
  s.add_dependency(%q<rubyzip>, [">= 1.1.7", "~> 1.1"])
84
- s.add_dependency(%q<very_tiny_state_machine>, ["~> 2"])
88
+ s.add_dependency(%q<terminal-table>, [">= 0"])
85
89
  s.add_dependency(%q<range_utils>, [">= 0"])
86
90
  s.add_dependency(%q<rack>, ["~> 1.6"])
87
91
  s.add_dependency(%q<rake>, ["~> 10.4"])
88
92
  s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
93
+ s.add_dependency(%q<coderay>, [">= 0"])
89
94
  s.add_dependency(%q<yard>, ["~> 0.8"])
90
95
  s.add_dependency(%q<bundler>, ["~> 1.0"])
91
96
  s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
92
97
  end
93
98
  else
94
99
  s.add_dependency(%q<rubyzip>, [">= 1.1.7", "~> 1.1"])
95
- s.add_dependency(%q<very_tiny_state_machine>, ["~> 2"])
100
+ s.add_dependency(%q<terminal-table>, [">= 0"])
96
101
  s.add_dependency(%q<range_utils>, [">= 0"])
97
102
  s.add_dependency(%q<rack>, ["~> 1.6"])
98
103
  s.add_dependency(%q<rake>, ["~> 10.4"])
99
104
  s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
105
+ s.add_dependency(%q<coderay>, [">= 0"])
100
106
  s.add_dependency(%q<yard>, ["~> 0.8"])
101
107
  s.add_dependency(%q<bundler>, ["~> 1.0"])
102
108
  s.add_dependency(%q<jeweler>, ["~> 2.0.1"])