zip_tricks 3.0.0 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -48
- data/lib/zip_tricks.rb +1 -1
- data/lib/zip_tricks/file_reader.rb +36 -5
- data/lib/zip_tricks/zip_writer.rb +4 -3
- data/spec/zip_tricks/file_reader_spec.rb +22 -0
- data/spec/zip_tricks/zip_writer_spec.rb +11 -0
- data/zip_tricks.gemspec +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d3b168fde79da25b24e3cb4b69caac818f1337f3
|
4
|
+
data.tar.gz: e1d7b9995636811c9c662a70e2caffb3a085709b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 65f27dacbcc5fd09e229f8543497fe3b3ff3da861696b355e01688f8b78dc7d02f4163de11e4a909c83f273a42005f63be7c1c7b7196f8657f0dd4d26cc4cc4f
|
7
|
+
data.tar.gz: 66b7db54cdc4702d0f867b8f263ea1bca2016405dd19db43bc4a7368ddb0d4b912b67344b7d0efc3156b0fbfe5868aa78b3cc6b8c22c87cddfd57282c8842dec
|
data/README.md
CHANGED
@@ -42,16 +42,20 @@ The block will only be called when actually sending the response to the client
|
|
42
42
|
|
43
43
|
Use the `SizeEstimator` to compute the correct size of the resulting archive.
|
44
44
|
|
45
|
+
# Prepare the response body. The block will only be called when the response starts to be written.
|
45
46
|
zip_body = ZipTricks::RackBody.new do | zip |
|
46
47
|
zip.add_stored_entry(filename: "myfile1.bin", size: 9090821, crc32: 12485)
|
47
48
|
zip << read_file('myfile1.bin')
|
48
49
|
zip.add_stored_entry(filename: "myfile2.bin", size: 458678, crc32: 89568)
|
49
50
|
zip << read_file('myfile2.bin')
|
50
51
|
end
|
52
|
+
|
53
|
+
# Precompute the Content-Length ahead of time
|
51
54
|
bytesize = ZipTricks::SizeEstimator.estimate do |z|
|
52
55
|
z.add_stored_entry(filename: 'myfile1.bin', size: 9090821)
|
53
56
|
z.add_stored_entry(filename: 'myfile2.bin', size: 458678)
|
54
57
|
end
|
58
|
+
|
55
59
|
[200, {'Content-Length' => bytesize.to_s}, zip_body]
|
56
60
|
|
57
61
|
## Other usage examples
|
@@ -79,55 +83,10 @@ to that socket using some accelerated writing technique, and only use the Stream
|
|
79
83
|
zip.simulate_write(my_temp_file.size)
|
80
84
|
end
|
81
85
|
|
82
|
-
##
|
83
|
-
|
84
|
-
Can be used to output a streamed ZIP archive directly through a Rack response body.
|
85
|
-
The block given to the constructor will be called when the response body will be read by the webserver,
|
86
|
-
and will receive a {ZipTricks::Streamer} as it's block argument. You can then add entries to the Streamer as usual.
|
87
|
-
The archive will be automatically closed at the end of the block.
|
88
|
-
|
89
|
-
# Precompute the Content-Length ahead of time
|
90
|
-
content_length = ZipTricks::SizeEstimator.estimate do | estimator |
|
91
|
-
estimator.add_stored_entry('large.tif', size=1289894)
|
92
|
-
end
|
93
|
-
|
94
|
-
# Prepare the response body. The block will only be called when the response starts to be written.
|
95
|
-
body = ZipTricks::RackBody.new do | streamer |
|
96
|
-
streamer.add_stored_entry('large.tif', size=1289894, crc32=198210)
|
97
|
-
streamer << large_file.read(1024*1024) until large_file.eof?
|
98
|
-
...
|
99
|
-
end
|
100
|
-
|
101
|
-
[200, {'Content-Type' => 'binary/octet-stream', 'Content-Length' => content_length.to_s}, body]
|
102
|
-
|
103
|
-
## BlockWrite
|
104
|
-
|
105
|
-
Can be used as the destination IO, but will call the given block instead on every call to `:<<`.
|
106
|
-
This can be used to attach the output of the zip compressor to the Rack response body, or another
|
107
|
-
destination. For Rack/Rails just use RackBody since it sets this up for you.
|
108
|
-
|
109
|
-
io = ZipTricks::BlockWrite.new{|data| socket << data }
|
110
|
-
ZipTricks::Streamer.open(io) do | zip |
|
111
|
-
zip.add_stored_entry("first-file.bin", raw_file.size, raw_file_crc32)
|
112
|
-
....
|
113
|
-
end
|
114
|
-
|
115
|
-
## SizeEstimator
|
116
|
-
|
117
|
-
Is used to predict the size of the ZIP archive after output. This can be used to generate, say, a `Content-Length` header,
|
118
|
-
or to predict the size of the resulting archive on the storage device. The size is estimated using a very fast "fake archiving"
|
119
|
-
procedure, so it computes the sizes of all the headers and the central directory very accurately.
|
120
|
-
|
121
|
-
expected_zip_archive_size = SizeEstimator.estimate do | estimator |
|
122
|
-
estimator.add_stored_entry("file.doc", size=898291)
|
123
|
-
estimator.add_compressed_entry("family.JPG", size=89281911, compressed_size=89218)
|
124
|
-
end
|
125
|
-
|
126
|
-
|
127
|
-
## StreamCRC32
|
86
|
+
## Computing the CRC32 value of a large file
|
128
87
|
|
129
|
-
|
130
|
-
library functions.
|
88
|
+
`BlockCRC32` computes the CRC32 checksum of an IO in a streaming fashion. It is slightly more convenient for the purpose
|
89
|
+
than using the raw Zlib library functions.
|
131
90
|
|
132
91
|
crc = ZipTricks::StreamCRC32.new
|
133
92
|
crc << large_file.read(1024 * 12) until large_file.eof?
|
data/lib/zip_tricks.rb
CHANGED
@@ -366,16 +366,47 @@ class ZipTricks::FileReader
|
|
366
366
|
# and a soft read (we might not be able to read as many bytes as we want)
|
367
367
|
file_io.seek(implied_position_of_eocd_record, IO::SEEK_SET)
|
368
368
|
str_containing_eocd_record = file_io.read(MAX_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE)
|
369
|
-
|
370
|
-
|
371
|
-
eocd_sig = [0x06054b50].pack(C_V)
|
372
|
-
eocd_idx_in_buf = str_containing_eocd_record.index(eocd_sig)
|
373
|
-
|
369
|
+
eocd_idx_in_buf = locate_eocd_signature(str_containing_eocd_record)
|
370
|
+
|
374
371
|
raise "Could not find the EOCD signature in the buffer - maybe a malformed ZIP file" unless eocd_idx_in_buf
|
375
372
|
|
376
373
|
implied_position_of_eocd_record + eocd_idx_in_buf
|
377
374
|
end
|
378
375
|
|
376
|
+
# This is tricky. Essentially, we have to scan the maximum possible number of bytes (that the EOCD can
|
377
|
+
# theoretically occupy including the comment), and we have to find a combination of:
|
378
|
+
# [EOCD signature, <some ZIP medatata>, comment byte size, the comment of that size, eof].
|
379
|
+
# The only way I could find to do this was with a sliding window, but there probably is a better way.
|
380
|
+
def locate_eocd_signature(in_str)
|
381
|
+
# We have to scan from the _very_ tail. We read the very minimum size
|
382
|
+
# the EOCD record can have (up to and including the comment size), using
|
383
|
+
# a sliding window. Once our end offset matches the comment size we found our
|
384
|
+
# EOCD marker.
|
385
|
+
eocd_signature_int = 0x06054b50
|
386
|
+
unpack_pattern = 'VvvvvVVv'
|
387
|
+
minimum_record_size = 22
|
388
|
+
end_location = minimum_record_size * -1
|
389
|
+
loop do
|
390
|
+
# If the window is nil, we have rolled off the start of the string, nothing to do here.
|
391
|
+
# We use negative values because if we used positive slice indices
|
392
|
+
# we would have to detect the rollover ourselves
|
393
|
+
break unless window = in_str[end_location, minimum_record_size]
|
394
|
+
|
395
|
+
window_location = in_str.bytesize + end_location
|
396
|
+
unpacked = window.unpack(unpack_pattern)
|
397
|
+
|
398
|
+
# If we found the signarue, pick up the comment size, and check if the size of the window
|
399
|
+
# plus that comment size is where we are in the string. If we are - bingo.
|
400
|
+
if unpacked[0] == 0x06054b50 && comment_size = unpacked[-1]
|
401
|
+
assumed_eocd_location = in_str.bytesize - comment_size - minimum_record_size
|
402
|
+
# if the comment size is where we should be at - we found our EOCD
|
403
|
+
return assumed_eocd_location if assumed_eocd_location == window_location
|
404
|
+
end
|
405
|
+
|
406
|
+
end_location -= 1 # Shift the window back, by one byte, and try again.
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
379
410
|
# Find the Zip64 EOCD locator segment offset. Do this by seeking backwards from the
|
380
411
|
# EOCD record in the archive by fixed offsets
|
381
412
|
def get_zip64_eocd_locator_offset(file_io, eocd_offset)
|
@@ -221,8 +221,9 @@ class ZipTricks::ZipWriter
|
|
221
221
|
# @param start_of_central_directory_location[Fixnum] byte offset of the start of central directory form the beginning of ZIP file
|
222
222
|
# @param central_directory_size[Fixnum] the size of the central directory (only file headers) in bytes
|
223
223
|
# @param num_files_in_archive[Fixnum] How many files the archive contains
|
224
|
+
# @param comment[String] the comment for the archive (defaults to ZIP_TRICKS_COMMENT)
|
224
225
|
# @return [void]
|
225
|
-
def write_end_of_central_directory(io:, start_of_central_directory_location:, central_directory_size:, num_files_in_archive:)
|
226
|
+
def write_end_of_central_directory(io:, start_of_central_directory_location:, central_directory_size:, num_files_in_archive:, comment: ZIP_TRICKS_COMMENT)
|
226
227
|
zip64_eocdr_offset = start_of_central_directory_location + central_directory_size
|
227
228
|
|
228
229
|
zip64_required = central_directory_size > FOUR_BYTE_MAX_UINT ||
|
@@ -296,8 +297,8 @@ class ZipTricks::ZipWriter
|
|
296
297
|
# directory with respect to
|
297
298
|
# the starting disk number 4 bytes
|
298
299
|
end
|
299
|
-
io << [
|
300
|
-
io <<
|
300
|
+
io << [comment.bytesize].pack(C_v) # .ZIP file comment length 2 bytes
|
301
|
+
io << comment # .ZIP file comment (variable size)
|
301
302
|
end
|
302
303
|
|
303
304
|
private
|
@@ -44,4 +44,26 @@ describe ZipTricks::FileReader do
|
|
44
44
|
expect(readback.bytesize).to eq(tolstoy.bytesize)
|
45
45
|
expect(readback[0..10]).to eq(tolstoy[0..10])
|
46
46
|
end
|
47
|
+
|
48
|
+
it 'is able to latch to the EOCD location even if the signature for the EOCD record appears all over the ZIP' do
|
49
|
+
# A VERY evil ZIP file which has this signature all over
|
50
|
+
eocd_sig = [0x06054b50].pack('V')
|
51
|
+
evil_str = "#{eocd_sig} and #{eocd_sig}"
|
52
|
+
|
53
|
+
z = StringIO.new
|
54
|
+
w = ZipTricks::ZipWriter.new
|
55
|
+
w.write_local_file_header(io: z, filename: evil_str, compressed_size: evil_str.bytesize,
|
56
|
+
uncompressed_size: evil_str.bytesize, crc32: 0x06054b50, gp_flags: 0, mtime: Time.now, storage_mode: 0)
|
57
|
+
z << evil_str
|
58
|
+
where = z.tell
|
59
|
+
w.write_central_directory_file_header(io: z, local_file_header_location: 0, gp_flags: 0, storage_mode: 0,
|
60
|
+
filename: evil_str, compressed_size: evil_str.bytesize,
|
61
|
+
uncompressed_size: evil_str.bytesize, mtime: Time.now, crc32: 0x06054b50)
|
62
|
+
w.write_end_of_central_directory(io: z, start_of_central_directory_location: where,
|
63
|
+
central_directory_size: z.tell - where, num_files_in_archive: 1, comment: evil_str)
|
64
|
+
|
65
|
+
z.rewind
|
66
|
+
entries = described_class.read_zip_structure(z)
|
67
|
+
expect(entries.length).to eq(1)
|
68
|
+
end
|
47
69
|
end
|
@@ -330,6 +330,17 @@ describe ZipTricks::ZipWriter do
|
|
330
330
|
expect(br.read_n(comment_length)).to match(/ZipTricks/)
|
331
331
|
end
|
332
332
|
|
333
|
+
it 'writes out the custom comment' do
|
334
|
+
buf = ''
|
335
|
+
comment = 'Ohai mate'
|
336
|
+
subject.write_end_of_central_directory(io: buf, start_of_central_directory_location: 9091211,
|
337
|
+
central_directory_size: 9091, num_files_in_archive: 4, comment: comment)
|
338
|
+
|
339
|
+
size_and_comment = buf[((comment.bytesize + 2) * -1)..-1]
|
340
|
+
comment_size = size_and_comment.unpack('v')[0]
|
341
|
+
expect(comment_size).to eq(comment.bytesize)
|
342
|
+
end
|
343
|
+
|
333
344
|
it 'writes out the Zip64 EOCD as well if the central directory is located beyound 4GB in the archive' do
|
334
345
|
buf = StringIO.new
|
335
346
|
|
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 3.
|
5
|
+
# stub: zip_tricks 3.1.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "zip_tricks"
|
9
|
-
s.version = "3.
|
9
|
+
s.version = "3.1.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-08-
|
14
|
+
s.date = "2016-08-16"
|
15
15
|
s.description = "Makes rubyzip stream, for real"
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
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: 3.
|
4
|
+
version: 3.1.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-08-
|
11
|
+
date: 2016-08-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|