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.
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'zip'
2
+ require 'very_tiny_state_machine'
3
+
4
+ module ZipTricks
5
+ VERSION = '2.5.0'
6
+
7
+ # Require all the sub-components except myself
8
+ Dir.glob(__dir__ + '/**/*.rb').sort.each {|p| require p unless p == __FILE__ }
9
+ end
@@ -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