zip_tricks 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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