zip_tricks 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|