zip_tricks 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +20 -0
- data/README.md +145 -0
- data/Rakefile +51 -0
- data/lib/zip_tricks/block_deflate.rb +89 -0
- data/lib/zip_tricks/block_write.rb +40 -0
- data/lib/zip_tricks/manifest.rb +85 -0
- data/lib/zip_tricks/null_writer.rb +7 -0
- data/lib/zip_tricks/rack_body.rb +41 -0
- data/lib/zip_tricks/stored_size_estimator.rb +44 -0
- data/lib/zip_tricks/stream_crc32.rb +43 -0
- data/lib/zip_tricks/streamer.rb +175 -0
- data/lib/zip_tricks/write_and_tell.rb +33 -0
- data/lib/zip_tricks.rb +9 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/zip_tricks/block_deflate_spec.rb +111 -0
- data/spec/zip_tricks/block_write_spec.rb +95 -0
- data/spec/zip_tricks/manifest_spec.rb +60 -0
- data/spec/zip_tricks/rack_body_spec.rb +34 -0
- data/spec/zip_tricks/stored_size_estimator_spec.rb +22 -0
- data/spec/zip_tricks/stream_crc32_spec.rb +38 -0
- data/spec/zip_tricks/streamer_spec.rb +253 -0
- data/spec/zip_tricks/war-and-peace.txt +10810 -0
- data/spec/zip_tricks/write_and_tell_spec.rb +43 -0
- data/zip_tricks.gemspec +90 -0
- metadata +192 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
# Is used to write streamed ZIP archives into the provided IO-ish object.
|
2
|
+
# The output IO is never going to be rewound or seeked, so the output
|
3
|
+
# of this object can be coupled directly to, say, a Rack output.
|
4
|
+
#
|
5
|
+
# Allows for splicing raw files (for "stored" entries without compression)
|
6
|
+
# and splicing of deflated files (for "deflated" storage mode).
|
7
|
+
#
|
8
|
+
# For stored entries, you need to know the CRC32 (as a uint) and the filesize upfront,
|
9
|
+
# before the writing of the entry body starts.
|
10
|
+
#
|
11
|
+
# For compressed entries, you need to know the bytesize of the precompressed entry
|
12
|
+
# as well.
|
13
|
+
class ZipTricks::Streamer
|
14
|
+
EntryBodySizeMismatch = Class.new(StandardError)
|
15
|
+
InvalidOutput = Class.new(ArgumentError)
|
16
|
+
|
17
|
+
# Language encoding flag (EFS) bit (general purpose bit 11)
|
18
|
+
EFS = 0b100000000000
|
19
|
+
|
20
|
+
# Default general purpose flags for each entry.
|
21
|
+
DEFAULT_GP_FLAGS = 0b00000000000
|
22
|
+
|
23
|
+
# Creates a new Streamer on top of the given IO-ish object and yields it. Once the given block
|
24
|
+
# returns, the Streamer will have it's `close` method called, which will write out the central
|
25
|
+
# directory of the archive to the output.
|
26
|
+
#
|
27
|
+
# @param stream [IO] the destination IO for the ZIP (should respond to `tell` and `<<`)
|
28
|
+
# @yield [Streamer] the streamer that can be written to
|
29
|
+
def self.open(stream)
|
30
|
+
archive = new(stream)
|
31
|
+
yield(archive)
|
32
|
+
archive.close
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a new Streamer on top of the given IO-ish object.
|
36
|
+
#
|
37
|
+
# @param stream [IO] the destination IO for the ZIP (should respond to `tell` and `<<`)
|
38
|
+
def initialize(stream)
|
39
|
+
raise InvalidOutput, "The stream should respond to #<<" unless stream.respond_to?(:<<)
|
40
|
+
stream = ZipTricks::WriteAndTell.new(stream) unless stream.respond_to?(:tell) && stream.respond_to?(:advance_position_by)
|
41
|
+
@output_stream = stream
|
42
|
+
|
43
|
+
@state_monitor = VeryTinyStateMachine.new(:before_entry, callbacks_to=self)
|
44
|
+
@state_monitor.permit_state :in_entry_header, :in_entry_body, :in_central_directory, :closed
|
45
|
+
@state_monitor.permit_transition :before_entry => :in_entry_header
|
46
|
+
@state_monitor.permit_transition :in_entry_header => :in_entry_body
|
47
|
+
@state_monitor.permit_transition :in_entry_body => :in_entry_header
|
48
|
+
@state_monitor.permit_transition :in_entry_body => :in_central_directory
|
49
|
+
@state_monitor.permit_transition :in_central_directory => :closed
|
50
|
+
|
51
|
+
@entry_set = ::Zip::EntrySet.new
|
52
|
+
end
|
53
|
+
|
54
|
+
# Writes a part of a zip entry body (actual binary data of the entry) into the output stream.
|
55
|
+
#
|
56
|
+
# @param binary_data [String] a String in binary encoding
|
57
|
+
# @return self
|
58
|
+
def <<(binary_data)
|
59
|
+
@state_monitor.transition_or_maintain! :in_entry_body
|
60
|
+
@output_stream << binary_data
|
61
|
+
@bytes_written_for_entry += binary_data.bytesize
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Advances the internal IO pointer to keep the offsets of the ZIP file in check. Use this if you are going
|
66
|
+
# to use accelerated writes to the socket (like the `sendfile()` call) after writing the headers, or if you
|
67
|
+
# just need to figure out the size of the archive.
|
68
|
+
#
|
69
|
+
# @param num_bytes [Numeric] how many bytes are going to be written bypassing the Streamer
|
70
|
+
# @return [Numeric] position in the output stream / ZIP archive
|
71
|
+
def simulate_write(num_bytes)
|
72
|
+
@state_monitor.transition_or_maintain! :in_entry_body
|
73
|
+
@output_stream.advance_position_by(num_bytes)
|
74
|
+
@bytes_written_for_entry += num_bytes
|
75
|
+
@output_stream.tell
|
76
|
+
end
|
77
|
+
|
78
|
+
# Writes out the local header for an entry (file in the ZIP) that is using the deflated storage model (is compressed).
|
79
|
+
# Once this method is called, the `<<` method has to be called to write the actual contents of the body.
|
80
|
+
#
|
81
|
+
# Note that the deflated body that is going to be written into the output has to be _precompressed_ (pre-deflated)
|
82
|
+
# before writing it into the Streamer, because otherwise it is impossible to know it's size upfront.
|
83
|
+
#
|
84
|
+
# @param entry_name [String] the name of the file in the entry
|
85
|
+
# @param uncompressed_size [Fixnum] the size of the entry when uncompressed, in bytes
|
86
|
+
# @param crc32 [Fixnum] the CRC32 checksum of the entry when uncompressed
|
87
|
+
# @param compressed_size [Fixnum] the size of the compressed entry that is going to be written into the archive
|
88
|
+
# @return [Fixnum] the offset the output IO is at after writing the entry header
|
89
|
+
def add_compressed_entry(entry_name, uncompressed_size, crc32, compressed_size)
|
90
|
+
@state_monitor.transition! :in_entry_header
|
91
|
+
|
92
|
+
entry = ::Zip::Entry.new(@file_name, entry_name)
|
93
|
+
entry.compression_method = Zip::Entry::DEFLATED
|
94
|
+
entry.crc = crc32
|
95
|
+
entry.size = uncompressed_size
|
96
|
+
entry.compressed_size = compressed_size
|
97
|
+
set_gp_flags_for_filename(entry, entry_name)
|
98
|
+
|
99
|
+
@entry_set << entry
|
100
|
+
entry.write_local_entry(@output_stream)
|
101
|
+
@expected_bytes_for_entry = compressed_size
|
102
|
+
@bytes_written_for_entry = 0
|
103
|
+
@output_stream.tell
|
104
|
+
end
|
105
|
+
|
106
|
+
# Writes out the local header for an entry (file in the ZIP) that is using the stored storage model (is stored as-is).
|
107
|
+
# Once this method is called, the `<<` method has to be called one or more times to write the actual contents of the body.
|
108
|
+
#
|
109
|
+
# @param entry_name [String] the name of the file in the entry
|
110
|
+
# @param uncompressed_size [Fixnum] the size of the entry when uncompressed, in bytes
|
111
|
+
# @param crc32 [Fixnum] the CRC32 checksum of the entry when uncompressed
|
112
|
+
# @return [Fixnum] the offset the output IO is at after writing the entry header
|
113
|
+
def add_stored_entry(entry_name, uncompressed_size, crc32)
|
114
|
+
@state_monitor.transition! :in_entry_header
|
115
|
+
|
116
|
+
entry = ::Zip::Entry.new(@file_name, entry_name)
|
117
|
+
entry.compression_method = Zip::Entry::STORED
|
118
|
+
entry.crc = crc32
|
119
|
+
entry.size = uncompressed_size
|
120
|
+
entry.compressed_size = uncompressed_size
|
121
|
+
set_gp_flags_for_filename(entry, entry_name)
|
122
|
+
@entry_set << entry
|
123
|
+
entry.write_local_entry(@output_stream)
|
124
|
+
@bytes_written_for_entry = 0
|
125
|
+
@expected_bytes_for_entry = uncompressed_size
|
126
|
+
@output_stream.tell
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# Writes out the global footer and the directory entry header and the global directory of the ZIP
|
131
|
+
# archive using the information about the entries added using `add_stored_entry` and `add_compressed_entry`.
|
132
|
+
#
|
133
|
+
# Once this method is called, the `Streamer` should be discarded (the ZIP archive is complete).
|
134
|
+
#
|
135
|
+
# @return [Fixnum] the offset the output IO is at after writing the central directory
|
136
|
+
def write_central_directory!
|
137
|
+
@state_monitor.transition! :in_central_directory
|
138
|
+
cdir = Zip::CentralDirectory.new(@entry_set, comment = nil)
|
139
|
+
cdir.write_to_stream(@output_stream)
|
140
|
+
@output_stream.tell
|
141
|
+
end
|
142
|
+
|
143
|
+
# Closes the archive. Writes the central directory if it has not yet been written.
|
144
|
+
# Switches the Streamer into a state where it can no longer be written to.
|
145
|
+
#
|
146
|
+
# Once this method is called, the `Streamer` should be discarded (the ZIP archive is complete).
|
147
|
+
#
|
148
|
+
# @return [Fixnum] the offset the output IO is at after closing the archive
|
149
|
+
def close
|
150
|
+
write_central_directory! unless @state_monitor.in_state?(:in_central_directory)
|
151
|
+
@state_monitor.transition! :closed
|
152
|
+
@output_stream.tell
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
# Set the general purpose flags for the entry. The only flag we care about is the EFS
|
158
|
+
# bit (bit 11) which should be set if the filename is UTF8. If it is, we need to set the
|
159
|
+
# bit so that the unarchiving application knows that the filename in the archive is UTF-8
|
160
|
+
# encoded, and not some DOS default. For ASCII entries it does not matter.
|
161
|
+
def set_gp_flags_for_filename(entry, filename)
|
162
|
+
filename.encode(Encoding::ASCII)
|
163
|
+
entry.gp_flags = DEFAULT_GP_FLAGS
|
164
|
+
rescue Encoding::UndefinedConversionError #=> UTF8 filename
|
165
|
+
entry.gp_flags = DEFAULT_GP_FLAGS | EFS
|
166
|
+
end
|
167
|
+
|
168
|
+
# Checks whether the number of bytes written conforms to the declared entry size
|
169
|
+
def leaving_in_entry_body_state
|
170
|
+
if @bytes_written_for_entry != @expected_bytes_for_entry
|
171
|
+
msg = "Wrong number of bytes written for entry (expected %d, got %d)" % [@expected_bytes_for_entry, @bytes_written_for_entry]
|
172
|
+
raise EntryBodySizeMismatch, msg
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# A tiny wrapper over any object that supports :<<.
|
2
|
+
# Adds :tell and :advance_position_by.
|
3
|
+
class ZipTricks::WriteAndTell
|
4
|
+
def initialize(io)
|
5
|
+
@io = io
|
6
|
+
@pos = 0
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(bytes)
|
10
|
+
return self if bytes.nil?
|
11
|
+
binary_bytes = binary(bytes)
|
12
|
+
@io << binary_bytes
|
13
|
+
@pos += binary_bytes.bytesize
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def advance_position_by(num_bytes)
|
18
|
+
@pos += num_bytes
|
19
|
+
end
|
20
|
+
|
21
|
+
def tell
|
22
|
+
@pos
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def binary(str)
|
28
|
+
return str if str.encoding == Encoding::BINARY
|
29
|
+
str.force_encoding(Encoding::BINARY)
|
30
|
+
rescue RuntimeError # the string is frozen
|
31
|
+
str.dup.force_encoding(Encoding::BINARY)
|
32
|
+
end
|
33
|
+
end
|
data/lib/zip_tricks.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
require 'zip_tricks'
|
6
|
+
require 'digest'
|
7
|
+
|
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}
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::BlockDeflate do
|
4
|
+
def tag_deflated(deflated_string, raw_string)
|
5
|
+
[120, 156].pack("C*") + deflated_string + [3,0].pack("C*") + [Zlib.adler32(raw_string)].pack("N")
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '.deflate_chunk' do
|
9
|
+
it 'compresses a blob that can be inflated later, when the header, footer and adler32 are added' do
|
10
|
+
blob = 'compressible' * 1024 * 4
|
11
|
+
compressed = described_class.deflate_chunk(blob)
|
12
|
+
expect(compressed.bytesize).to be < blob.bytesize
|
13
|
+
complete_deflated_segment = tag_deflated(compressed, blob)
|
14
|
+
expect(Zlib.inflate(complete_deflated_segment)).to eq(blob)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'removes the header' do
|
18
|
+
blob = 'compressible' * 1024 * 4
|
19
|
+
compressed = described_class.deflate_chunk(blob)
|
20
|
+
expect(compressed[0..1]).not_to eq([120, 156].pack("C*"))
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'removes the adler32' do
|
24
|
+
blob = 'compressible' * 1024 * 4
|
25
|
+
compressed = described_class.deflate_chunk(blob)
|
26
|
+
adler = [Zlib.adler32(blob)].pack("N")
|
27
|
+
expect(compressed).not_to end_with(adler)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'removes the end marker' do
|
31
|
+
blob = 'compressible' * 1024 * 4
|
32
|
+
compressed = described_class.deflate_chunk(blob)
|
33
|
+
expect(compressed[-7..-5]).not_to eq([3,0].pack("C*"))
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'honors the compression level' do
|
37
|
+
deflater = Zlib::Deflate.new
|
38
|
+
expect(Zlib::Deflate).to receive(:new).with(2) { deflater }
|
39
|
+
blob = 'compressible' * 1024 * 4
|
40
|
+
compressed = described_class.deflate_chunk(blob, level: 2)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'deflate_in_blocks_and_terminate' do
|
45
|
+
it 'uses deflate_in_blocks' do
|
46
|
+
data = 'compressible' * 1024 * 1024 * 10
|
47
|
+
input = StringIO.new(data)
|
48
|
+
output = StringIO.new
|
49
|
+
block_size = 1024 * 64
|
50
|
+
expect(described_class).to receive(:deflate_in_blocks).with(input, output, level: -1, block_size: block_size).and_call_original
|
51
|
+
described_class.deflate_in_blocks_and_terminate(input, output, block_size: block_size)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'passes a custom compression level' do
|
55
|
+
data = 'compressible' * 1024 * 1024 * 10
|
56
|
+
input = StringIO.new(data)
|
57
|
+
output = StringIO.new
|
58
|
+
expect(described_class).to receive(:deflate_in_blocks).with(input, output, level: 9, block_size: 5242880).and_call_original
|
59
|
+
described_class.deflate_in_blocks_and_terminate(input, output, level: Zlib::BEST_COMPRESSION)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'writes the end marker' do
|
63
|
+
data = 'compressible' * 1024 * 1024 * 10
|
64
|
+
input = StringIO.new(data)
|
65
|
+
output = StringIO.new
|
66
|
+
described_class.deflate_in_blocks_and_terminate(input, output)
|
67
|
+
expect(output.string[-2..-1]).to eq([3,0].pack("C*"))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '.write_terminator' do
|
72
|
+
it 'writes the terminator and returns 2 for number of bytes written' do
|
73
|
+
buf = double('IO')
|
74
|
+
expect(buf).to receive(:<<).with([3,0].pack("C*"))
|
75
|
+
expect(described_class.write_terminator(buf)).to eq(2)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '.deflate_in_blocks' do
|
80
|
+
it 'honors the block size' do
|
81
|
+
data = 'compressible' * 1024 * 1024 * 10
|
82
|
+
input = StringIO.new(data)
|
83
|
+
output = StringIO.new
|
84
|
+
block_size = 1024 * 64
|
85
|
+
|
86
|
+
num_chunks = (data.bytesize / block_size.to_f).ceil
|
87
|
+
expect(described_class).to receive(:deflate_chunk).exactly(num_chunks).times.and_call_original
|
88
|
+
expect(input).to receive(:read).with(block_size).exactly(num_chunks + 1).times.and_call_original
|
89
|
+
expect(output).to receive(:<<).exactly(num_chunks).times.and_call_original
|
90
|
+
|
91
|
+
described_class.deflate_in_blocks(input, output, block_size: block_size)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'does not write the end marker' do
|
95
|
+
input_string = 'compressible' * 1024 * 1024 * 10
|
96
|
+
output_string = ''
|
97
|
+
|
98
|
+
described_class.deflate_in_blocks(StringIO.new(input_string), StringIO.new(output_string))
|
99
|
+
expect(output_string).not_to be_empty
|
100
|
+
expect(output_string).not_to end_with([3,0].pack("C*"))
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'returns the number of bytes written' do
|
104
|
+
input_string = 'compressible' * 1024 * 1024 * 10
|
105
|
+
output_string = ''
|
106
|
+
|
107
|
+
num_bytes = described_class.deflate_in_blocks(StringIO.new(input_string), StringIO.new(output_string))
|
108
|
+
expect(num_bytes).to eq(245016)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::BlockWrite do
|
4
|
+
it 'calls the given block each time data is written' do
|
5
|
+
blobs = []
|
6
|
+
adapter = described_class.new{|s|
|
7
|
+
blobs << s
|
8
|
+
}
|
9
|
+
|
10
|
+
adapter << 'hello'
|
11
|
+
adapter << 'world'
|
12
|
+
adapter << '!'
|
13
|
+
|
14
|
+
expect(blobs).to eq(['hello', 'world', '!'])
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'supports chained shovel' do
|
18
|
+
blobs = []
|
19
|
+
adapter = described_class.new{|s|
|
20
|
+
blobs << s
|
21
|
+
}
|
22
|
+
|
23
|
+
adapter << 'hello' << 'world' << '!'
|
24
|
+
|
25
|
+
expect(blobs).to eq(['hello', 'world', '!'])
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'can write in all possible encodings, even if the strings are frozen' do
|
29
|
+
destination = ''.encode(Encoding::BINARY)
|
30
|
+
|
31
|
+
accum_string = ''
|
32
|
+
adapter = described_class.new{|s| accum_string << s }
|
33
|
+
|
34
|
+
adapter << 'hello'
|
35
|
+
adapter << 'привет'
|
36
|
+
adapter << 'привет'.freeze
|
37
|
+
adapter << '!'
|
38
|
+
adapter << SecureRandom.random_bytes(1024)
|
39
|
+
|
40
|
+
expect(accum_string.bytesize).to eq(1054)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'can be closed' do
|
44
|
+
expect(described_class.new{}.close).to be_nil
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'forces the written strings to binary encoding' do
|
48
|
+
blobs = []
|
49
|
+
adapter = described_class.new{|s|
|
50
|
+
blobs << s
|
51
|
+
}
|
52
|
+
adapter << 'hello'.encode(Encoding::UTF_8)
|
53
|
+
adapter << 'world'.encode(Encoding::BINARY)
|
54
|
+
adapter << '!'
|
55
|
+
expect(blobs).not_to be_empty
|
56
|
+
blobs.each {|s| expect(s.encoding).to eq(Encoding::BINARY) }
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'omits strings of zero length' do
|
60
|
+
blobs = []
|
61
|
+
adapter = described_class.new{|s|
|
62
|
+
blobs << s
|
63
|
+
}
|
64
|
+
adapter << 'hello'
|
65
|
+
adapter << ''
|
66
|
+
adapter << '!'
|
67
|
+
expect(blobs).to eq(['hello', '!'])
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'omits nils' do
|
71
|
+
blobs = []
|
72
|
+
adapter = described_class.new{|s|
|
73
|
+
blobs << s
|
74
|
+
}
|
75
|
+
adapter << 'hello'
|
76
|
+
adapter << nil
|
77
|
+
adapter << '!'
|
78
|
+
expect(blobs).to eq(['hello', '!'])
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'raises a TypeError on specific unsupported methods' do
|
82
|
+
adapter = described_class.new {|s| }
|
83
|
+
expect {
|
84
|
+
adapter.seek(123)
|
85
|
+
}.to raise_error(/non\-rewindable/)
|
86
|
+
|
87
|
+
expect {
|
88
|
+
adapter.to_s
|
89
|
+
}.to raise_error(/non\-rewindable/)
|
90
|
+
|
91
|
+
expect {
|
92
|
+
adapter.pos = 123
|
93
|
+
}.to raise_error(/non\-rewindable/)
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::Manifest do
|
4
|
+
it 'builds a map of the contained ranges, and has its cumulative size match the predicted archive size exactly' do
|
5
|
+
# Generate a couple of random files
|
6
|
+
raw_file_1 = SecureRandom.random_bytes(1024 * 20)
|
7
|
+
raw_file_2 = SecureRandom.random_bytes(1024 * 128)
|
8
|
+
raw_file_3 = SecureRandom.random_bytes(1258695)
|
9
|
+
|
10
|
+
manifest, bytesize = described_class.build do | builder |
|
11
|
+
r = builder.add_stored_entry(name: "first-file.bin", size_uncompressed: raw_file_1.size)
|
12
|
+
expect(r).to eq(builder), "add_stored_entry should return self"
|
13
|
+
|
14
|
+
builder.add_stored_entry(name: "second-file.bin", size_uncompressed: raw_file_2.size)
|
15
|
+
|
16
|
+
r = builder.add_compressed_entry(name: "second-file-comp.bin", size_uncompressed: raw_file_2.size,
|
17
|
+
size_compressed: raw_file_3.size, segment_info: 'http://example.com/second-file-deflated-segment.bin')
|
18
|
+
expect(r).to eq(builder), "add_compressed_entry should return self"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'range_utils'
|
22
|
+
|
23
|
+
expect(manifest).to be_kind_of(Array)
|
24
|
+
total_size_of_all_parts = manifest.inject(0) do | total_bytes, span |
|
25
|
+
total_bytes + RangeUtils.size_from_range(span.byte_range_in_zip)
|
26
|
+
end
|
27
|
+
expect(total_size_of_all_parts).to eq(1410595)
|
28
|
+
expect(bytesize).to eq(1410595)
|
29
|
+
|
30
|
+
expect(manifest.length).to eq(7)
|
31
|
+
|
32
|
+
first_header = manifest[0]
|
33
|
+
expect(first_header.part_type).to eq(:entry_header)
|
34
|
+
expect(first_header.byte_range_in_zip).to eq(0..43)
|
35
|
+
expect(first_header.filename).to eq("first-file.bin")
|
36
|
+
expect(first_header.additional_metadata).to be_nil
|
37
|
+
|
38
|
+
first_body = manifest[1]
|
39
|
+
expect(first_body.part_type).to eq(:entry_body)
|
40
|
+
expect(first_body.byte_range_in_zip).to eq(44..20523)
|
41
|
+
expect(first_body.filename).to eq("first-file.bin")
|
42
|
+
expect(first_body.additional_metadata).to be_nil
|
43
|
+
|
44
|
+
third_header = manifest[4]
|
45
|
+
expect(third_header.part_type).to eq(:entry_header)
|
46
|
+
expect(third_header.byte_range_in_zip).to eq(151641..151690)
|
47
|
+
expect(third_header.filename).to eq("second-file-comp.bin")
|
48
|
+
expect(third_header.additional_metadata).to eq("http://example.com/second-file-deflated-segment.bin")
|
49
|
+
|
50
|
+
third_body = manifest[5]
|
51
|
+
expect(third_body.part_type).to eq(:entry_body)
|
52
|
+
expect(third_body.byte_range_in_zip).to eq(151691..1410385)
|
53
|
+
expect(third_body.filename).to eq("second-file-comp.bin")
|
54
|
+
expect(third_body.additional_metadata).to eq("http://example.com/second-file-deflated-segment.bin")
|
55
|
+
|
56
|
+
cd = manifest[-1]
|
57
|
+
expect(cd.part_type).to eq(:central_directory)
|
58
|
+
expect(cd.byte_range_in_zip).to eq(1410386..1410594)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::RackBody do
|
4
|
+
it 'is usable as a Rack response body, supports each() and close()' do
|
5
|
+
output_buf = Tempfile.new('output')
|
6
|
+
|
7
|
+
file_body = SecureRandom.random_bytes(1024 * 1024 + 8981)
|
8
|
+
|
9
|
+
body = described_class.new do | zip |
|
10
|
+
zip.add_stored_entry("A file", file_body.bytesize, Zlib.crc32(file_body))
|
11
|
+
zip << file_body
|
12
|
+
end
|
13
|
+
|
14
|
+
body.each do | some_data |
|
15
|
+
output_buf << some_data
|
16
|
+
end
|
17
|
+
body.close
|
18
|
+
|
19
|
+
output_buf.rewind
|
20
|
+
expect(output_buf.size).to eq(1057667)
|
21
|
+
|
22
|
+
per_filename = {}
|
23
|
+
Zip::File.open(output_buf.path) do |zip_file|
|
24
|
+
# Handle entries one by one
|
25
|
+
zip_file.each do |entry|
|
26
|
+
# The entry name gets returned with a binary encoding, we have to force it back.
|
27
|
+
per_filename[entry.name] = entry.get_input_stream.read
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
expect(per_filename).to have_key('A file')
|
32
|
+
expect(per_filename['A file'].bytesize).to eq(file_body.bytesize)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::StoredSizeEstimator do
|
4
|
+
it 'accurately predicts the output zip size' do
|
5
|
+
# Generate a couple of random files
|
6
|
+
raw_file_1 = SecureRandom.random_bytes(1024 * 20)
|
7
|
+
raw_file_2 = SecureRandom.random_bytes(1024 * 128)
|
8
|
+
raw_file_3 = SecureRandom.random_bytes(1258695)
|
9
|
+
|
10
|
+
predicted_size = described_class.perform_fake_archiving do | estimator |
|
11
|
+
r = estimator.add_stored_entry("first-file.bin", raw_file_1.size)
|
12
|
+
expect(r).to eq(estimator), "add_stored_entry should return self"
|
13
|
+
|
14
|
+
estimator.add_stored_entry("second-file.bin", raw_file_2.size)
|
15
|
+
|
16
|
+
r = estimator.add_compressed_entry("second-file.bin", raw_file_2.size, raw_file_3.size)
|
17
|
+
expect(r).to eq(estimator), "add_compressed_entry should return self"
|
18
|
+
end
|
19
|
+
|
20
|
+
expect(predicted_size).to eq(1410524)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe ZipTricks::StreamCRC32 do
|
4
|
+
it 'computes the CRC32 of a large binary file' do
|
5
|
+
raw = StringIO.new(SecureRandom.random_bytes(45 * 1024 * 1024))
|
6
|
+
crc = Zlib.crc32(raw.string)
|
7
|
+
via_from_io = described_class.from_io(raw)
|
8
|
+
expect(via_from_io).to eq(crc)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'allows in-place updates' do
|
12
|
+
raw = StringIO.new(SecureRandom.random_bytes(45 * 1024 * 1024))
|
13
|
+
crc = Zlib.crc32(raw.string)
|
14
|
+
|
15
|
+
stream_crc = described_class.new
|
16
|
+
stream_crc << raw.read(1024 * 64) until raw.eof?
|
17
|
+
expect(stream_crc.to_i).to eq(crc)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'supports chained shovel' do
|
21
|
+
str = 'abcdef'
|
22
|
+
crc = Zlib.crc32(str)
|
23
|
+
|
24
|
+
stream_crc = described_class.new
|
25
|
+
stream_crc << 'a' << 'b' << 'c' << 'd' << 'e' << 'f'
|
26
|
+
|
27
|
+
expect(stream_crc.to_i).to eq(crc)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'allows in-place update with a known value' do
|
31
|
+
crc = Zlib.crc32
|
32
|
+
|
33
|
+
stream_crc = described_class.new
|
34
|
+
stream_crc << "This is some data"
|
35
|
+
stream_crc.append(45678, 12910)
|
36
|
+
expect(stream_crc.to_i).to eq(1555667875)
|
37
|
+
end
|
38
|
+
end
|