zip_tricks 2.8.1 → 3.0.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.
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"])