zip_tricks 4.5.2 → 4.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -78
- data/.travis.yml +8 -4
- data/CHANGELOG.md +14 -2
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +151 -0
- data/README.md +1 -1
- data/bench/buffered_crc32_bench.rb +111 -0
- data/examples/rack_application.rb +1 -1
- data/lib/zip_tricks/file_reader.rb +104 -126
- data/lib/zip_tricks/remote_io.rb +2 -6
- data/lib/zip_tricks/stream_crc32.rb +2 -16
- data/lib/zip_tricks/streamer.rb +29 -16
- data/lib/zip_tricks/streamer/deflated_writer.rb +34 -11
- data/lib/zip_tricks/streamer/entry.rb +0 -1
- data/lib/zip_tricks/streamer/stored_writer.rb +21 -8
- data/lib/zip_tricks/version.rb +1 -1
- data/lib/zip_tricks/write_buffer.rb +49 -0
- data/lib/zip_tricks/zip_writer.rb +138 -132
- data/qa/README_QA.md +16 -0
- data/{testing → qa}/generate_test_files.rb +0 -0
- data/{testing → qa}/in/VTYL8830.jpg +0 -0
- data/{testing → qa}/in/war-and-peace.txt +0 -0
- data/{testing → qa}/support.rb +3 -3
- data/{testing → qa}/test-report-2016-07-28.txt +0 -0
- data/{testing → qa}/test-report-2016-12-12.txt +0 -0
- data/{testing → qa}/test-report-2017-04-2.txt +0 -0
- data/{testing → qa}/test-report.txt +0 -0
- data/zip_tricks.gemspec +2 -3
- metadata +33 -16
- data/.rubocop_todo.yml +0 -43
- data/testing/README_TESTING.md +0 -12
data/lib/zip_tricks/streamer.rb
CHANGED
@@ -162,14 +162,17 @@ class ZipTricks::Streamer
|
|
162
162
|
# Streamer, because otherwise it is impossible to know it's size upfront.
|
163
163
|
#
|
164
164
|
# @param filename [String] the name of the file in the entry
|
165
|
+
# @param modification_time [Time] the modification time of the file in the archive
|
165
166
|
# @param compressed_size [Integer] the size of the compressed entry that
|
166
167
|
# is going to be written into the archive
|
167
168
|
# @param uncompressed_size [Integer] the size of the entry when uncompressed, in bytes
|
168
169
|
# @param crc32 [Integer] the CRC32 checksum of the entry when uncompressed
|
169
170
|
# @param use_data_descriptor [Boolean] whether the entry body will be followed by a data descriptor
|
170
171
|
# @return [Integer] the offset the output IO is at after writing the entry header
|
171
|
-
def add_deflated_entry(filename:, compressed_size: 0, uncompressed_size: 0, crc32: 0, use_data_descriptor: false)
|
172
|
-
add_file_and_write_local_header(filename: filename,
|
172
|
+
def add_deflated_entry(filename:, modification_time: Time.now.utc, compressed_size: 0, uncompressed_size: 0, crc32: 0, use_data_descriptor: false)
|
173
|
+
add_file_and_write_local_header(filename: filename,
|
174
|
+
modification_time: modification_time,
|
175
|
+
crc32: crc32,
|
173
176
|
storage_mode: DEFLATED,
|
174
177
|
compressed_size: compressed_size,
|
175
178
|
uncompressed_size: uncompressed_size,
|
@@ -186,12 +189,14 @@ class ZipTricks::Streamer
|
|
186
189
|
# times to write the actual contents of the body.
|
187
190
|
#
|
188
191
|
# @param filename [String] the name of the file in the entry
|
192
|
+
# @param modification_time [Time] the modification time of the file in the archive
|
189
193
|
# @param size [Integer] the size of the file when uncompressed, in bytes
|
190
194
|
# @param crc32 [Integer] the CRC32 checksum of the entry when uncompressed
|
191
195
|
# @param use_data_descriptor [Boolean] whether the entry body will be followed by a data descriptor. When in use
|
192
196
|
# @return [Integer] the offset the output IO is at after writing the entry header
|
193
|
-
def add_stored_entry(filename:, size: 0, crc32: 0, use_data_descriptor: false)
|
197
|
+
def add_stored_entry(filename:, modification_time: Time.now.utc, size: 0, crc32: 0, use_data_descriptor: false)
|
194
198
|
add_file_and_write_local_header(filename: filename,
|
199
|
+
modification_time: modification_time,
|
195
200
|
crc32: crc32,
|
196
201
|
storage_mode: STORED,
|
197
202
|
compressed_size: size,
|
@@ -203,9 +208,11 @@ class ZipTricks::Streamer
|
|
203
208
|
# Adds an empty directory to the archive with a size of 0 and permissions of 755.
|
204
209
|
#
|
205
210
|
# @param dirname [String] the name of the directory in the archive
|
211
|
+
# @param modification_time [Time] the modification time of the directory in the archive
|
206
212
|
# @return [Integer] the offset the output IO is at after writing the entry header
|
207
|
-
def add_empty_directory(dirname:)
|
213
|
+
def add_empty_directory(dirname:, modification_time: Time.now.utc)
|
208
214
|
add_file_and_write_local_header(filename: dirname.to_s + '/',
|
215
|
+
modification_time: modification_time,
|
209
216
|
crc32: 0,
|
210
217
|
storage_mode: STORED,
|
211
218
|
compressed_size: 0,
|
@@ -246,10 +253,12 @@ class ZipTricks::Streamer
|
|
246
253
|
# and attention is recommended.
|
247
254
|
#
|
248
255
|
# @param filename[String] the name of the file in the archive
|
256
|
+
# @param modification_time [Time] the modification time of the file in the archive
|
249
257
|
# @yield [#<<, #write] an object that the file contents must be written to that will be automatically closed
|
250
258
|
# @return [#<<, #write, #close] an object that the file contents must be written to, has to be closed manually
|
251
|
-
def write_stored_file(filename)
|
259
|
+
def write_stored_file(filename, modification_time: Time.now.utc)
|
252
260
|
add_stored_entry(filename: filename,
|
261
|
+
modification_time: modification_time,
|
253
262
|
use_data_descriptor: true,
|
254
263
|
crc32: 0,
|
255
264
|
size: 0)
|
@@ -296,9 +305,11 @@ class ZipTricks::Streamer
|
|
296
305
|
# and attention is recommended.
|
297
306
|
#
|
298
307
|
# @param filename[String] the name of the file in the archive
|
308
|
+
# @param modification_time [Time] the modification time of the file in the archive
|
299
309
|
# @yield [#<<, #write] an object that the file contents must be written to
|
300
|
-
def write_deflated_file(filename)
|
310
|
+
def write_deflated_file(filename, modification_time: Time.now.utc)
|
301
311
|
add_deflated_entry(filename: filename,
|
312
|
+
modification_time: modification_time,
|
302
313
|
use_data_descriptor: true,
|
303
314
|
crc32: 0,
|
304
315
|
compressed_size: 0,
|
@@ -388,20 +399,20 @@ class ZipTricks::Streamer
|
|
388
399
|
|
389
400
|
private
|
390
401
|
|
391
|
-
def add_file_and_write_local_header(
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
402
|
+
def add_file_and_write_local_header(
|
403
|
+
filename:,
|
404
|
+
modification_time:,
|
405
|
+
crc32:,
|
406
|
+
storage_mode:,
|
407
|
+
compressed_size:,
|
408
|
+
uncompressed_size:,
|
409
|
+
use_data_descriptor:)
|
397
410
|
|
398
411
|
# Clean backslashes and uniqify filenames if there are duplicates
|
399
412
|
filename = remove_backslash(filename)
|
400
413
|
filename = uniquify_name(filename) if @filenames_set.include?(filename)
|
401
414
|
|
402
|
-
unless [STORED, DEFLATED].include?(storage_mode)
|
403
|
-
raise UnknownMode, "Unknown compression mode #{storage_mode}"
|
404
|
-
end
|
415
|
+
raise UnknownMode, "Unknown compression mode #{storage_mode}" unless [STORED, DEFLATED].include?(storage_mode)
|
405
416
|
|
406
417
|
raise Overflow, 'Filename is too long' if filename.bytesize > 0xFFFF
|
407
418
|
|
@@ -416,11 +427,13 @@ class ZipTricks::Streamer
|
|
416
427
|
compressed_size,
|
417
428
|
uncompressed_size,
|
418
429
|
storage_mode,
|
419
|
-
|
430
|
+
modification_time,
|
420
431
|
use_data_descriptor)
|
432
|
+
|
421
433
|
@files << e
|
422
434
|
@filenames_set << e.filename
|
423
435
|
@local_header_offsets << @out.tell
|
436
|
+
|
424
437
|
@writer.write_local_file_header(io: @out,
|
425
438
|
gp_flags: e.gp_flags,
|
426
439
|
crc32: e.crc32,
|
@@ -1,39 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Sends writes to the given `io` compressed using a {Zlib::Deflate}. Also
|
4
|
+
# registers data passing through it in a CRC32 checksum calculator. Is made to be completely
|
5
|
+
# interchangeable with the StoredWriter in terms of interface.
|
3
6
|
class ZipTricks::Streamer::DeflatedWriter
|
4
7
|
# After how many bytes of incoming data the deflater for the
|
5
8
|
# contents must be flushed. This is done to prevent unreasonable
|
6
|
-
# memory use when archiving large files
|
9
|
+
# memory use when archiving large files, and to ensure we write to
|
10
|
+
# the socket often enough while still maintaining acceptable
|
11
|
+
# compression
|
7
12
|
FLUSH_EVERY_N_BYTES = 1024 * 1024 * 5
|
8
13
|
|
14
|
+
# The amount of bytes we will buffer before computing the intermediate
|
15
|
+
# CRC32 checksums. Benchmarks show that the optimum is 64KB (see
|
16
|
+
# `bench/buffered_crc32_bench.rb), if that is exceeded Zlib is going
|
17
|
+
# to perform internal CRC combine calls which will make the speed go down again.
|
18
|
+
CRC32_BUFFER_SIZE = 64 * 1024
|
19
|
+
|
9
20
|
def initialize(io)
|
10
|
-
@
|
21
|
+
@compressed_io = ZipTricks::WriteAndTell.new(io)
|
11
22
|
@uncompressed_size = 0
|
12
|
-
@started_at = @io.tell
|
13
|
-
@crc = ZipTricks::StreamCRC32.new
|
14
23
|
@deflater = ::Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -::Zlib::MAX_WBITS)
|
24
|
+
@crc = ZipTricks::WriteBuffer.new(ZipTricks::StreamCRC32.new, CRC32_BUFFER_SIZE)
|
15
25
|
@bytes_since_last_flush = 0
|
16
26
|
end
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
28
|
+
# Writes the given data into the deflater, and flushes the deflater
|
29
|
+
# after having written more than FLUSH_EVERY_N_BYTES bytes of data
|
30
|
+
#
|
31
|
+
# @param data[String] data to be written
|
32
|
+
# @return self
|
23
33
|
def <<(data)
|
24
34
|
@uncompressed_size += data.bytesize
|
25
35
|
@bytes_since_last_flush += data.bytesize
|
26
|
-
@
|
36
|
+
@compressed_io << @deflater.deflate(data)
|
27
37
|
@crc << data
|
38
|
+
|
28
39
|
interim_flush
|
40
|
+
|
29
41
|
self
|
30
42
|
end
|
31
43
|
|
44
|
+
# Returns the amount of data received for writing, the amount of
|
45
|
+
# compressed data written and the CRC32 checksum. The return value
|
46
|
+
# can be directly used as the argument to {Streamer#update_last_entry_and_write_data_descriptor}
|
47
|
+
#
|
48
|
+
# @param data[String] data to be written
|
49
|
+
# @return [Hash] a hash of `{crc32, compressed_size, uncompressed_size}`
|
50
|
+
def finish
|
51
|
+
@compressed_io << @deflater.finish until @deflater.finished?
|
52
|
+
{crc32: @crc.to_i, compressed_size: @compressed_io.tell, uncompressed_size: @uncompressed_size}
|
53
|
+
end
|
54
|
+
|
32
55
|
private
|
33
56
|
|
34
57
|
def interim_flush
|
35
58
|
return if @bytes_since_last_flush < FLUSH_EVERY_N_BYTES
|
36
|
-
@
|
59
|
+
@compressed_io << @deflater.flush
|
37
60
|
@bytes_since_last_flush = 0
|
38
61
|
end
|
39
62
|
end
|
@@ -8,7 +8,6 @@ class ZipTricks::Streamer::Entry < Struct.new(:filename, :crc32, :compressed_siz
|
|
8
8
|
def initialize(*)
|
9
9
|
super
|
10
10
|
filename.force_encoding(Encoding::UTF_8)
|
11
|
-
# Rubocop: convention: Avoid using rescue in its modifier form.
|
12
11
|
@requires_efs_flag = !(begin
|
13
12
|
filename.encode(Encoding::ASCII)
|
14
13
|
rescue
|
@@ -1,23 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# Sends writes to the given `io`, and also registers all the data passing
|
4
|
+
# through it in a CRC32 checksum calculator. Is made to be completely
|
5
|
+
# interchangeable with the DeflatedWriter in terms of interface.
|
4
6
|
class ZipTricks::Streamer::StoredWriter
|
7
|
+
# The amount of bytes we will buffer before computing the intermediate
|
8
|
+
# CRC32 checksums. Benchmarks show that the optimum is 64KB (see
|
9
|
+
# `bench/buffered_crc32_bench.rb), if that is exceeded Zlib is going
|
10
|
+
# to perform internal CRC combine calls which will make the speed go down again.
|
11
|
+
CRC32_BUFFER_SIZE = 64 * 1024
|
12
|
+
|
5
13
|
def initialize(io)
|
6
|
-
@io = io
|
7
|
-
@
|
8
|
-
@compressed_size = 0
|
9
|
-
@started_at = @io.tell
|
10
|
-
@crc = ZipTricks::StreamCRC32.new
|
14
|
+
@io = ZipTricks::WriteAndTell.new(io)
|
15
|
+
@crc = ZipTricks::WriteBuffer.new(ZipTricks::StreamCRC32.new, CRC32_BUFFER_SIZE)
|
11
16
|
end
|
12
17
|
|
18
|
+
# Writes the given data to the contained IO object.
|
19
|
+
#
|
20
|
+
# @param data[String] data to be written
|
21
|
+
# @return self
|
13
22
|
def <<(data)
|
14
23
|
@io << data
|
15
24
|
@crc << data
|
16
25
|
self
|
17
26
|
end
|
18
27
|
|
28
|
+
# Returns the amount of data written and the CRC32 checksum. The return value
|
29
|
+
# can be directly used as the argument to {Streamer#update_last_entry_and_write_data_descriptor}
|
30
|
+
#
|
31
|
+
# @param data[String] data to be written
|
32
|
+
# @return [Hash] a hash of `{crc32, compressed_size, uncompressed_size}`
|
19
33
|
def finish
|
20
|
-
|
21
|
-
{crc32: @crc.to_i, compressed_size: size, uncompressed_size: size}
|
34
|
+
{crc32: @crc.to_i, compressed_size: @io.tell, uncompressed_size: @io.tell}
|
22
35
|
end
|
23
36
|
end
|
data/lib/zip_tricks/version.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Some operations (such as CRC32) benefit when they are performed
|
2
|
+
# on larger chunks of data. In certain use cases, it is possible that
|
3
|
+
# the consumer of ZipTricks is going to be writing small chunks
|
4
|
+
# in rapid succession, so CRC32 is going to have to perform a lot of
|
5
|
+
# CRC32 combine operations - and this adds up. Since the CRC32 value
|
6
|
+
# is usually not needed until the complete output has completed
|
7
|
+
# we can buffer at least some amount of data before computing CRC32 over it.
|
8
|
+
class ZipTricks::WriteBuffer
|
9
|
+
# Creates a new WriteBuffer bypassing into a given writable object
|
10
|
+
#
|
11
|
+
# @param writable[#<<] An object that responds to `#<<` with string as argument
|
12
|
+
# @param buffer_size[Integer] How many bytes to buffer
|
13
|
+
def initialize(writable, buffer_size)
|
14
|
+
@buf = StringIO.new
|
15
|
+
@buffer_size = buffer_size
|
16
|
+
@writable = writable
|
17
|
+
end
|
18
|
+
|
19
|
+
# Appends the given data to the write buffer, and flushes the buffer into the
|
20
|
+
# writable if the buffer size exceeds the `buffer_size` given at initialization
|
21
|
+
#
|
22
|
+
# @param data[String] data to be written
|
23
|
+
# @return self
|
24
|
+
def <<(data)
|
25
|
+
@buf << data
|
26
|
+
flush! if @buf.size > @buffer_size
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Explicitly flushes the buffer if it contains anything
|
31
|
+
#
|
32
|
+
# @return self
|
33
|
+
def flush!
|
34
|
+
@writable << @buf.string if @buf.size > 0
|
35
|
+
@buf.truncate(0)
|
36
|
+
@buf.rewind
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Flushes the buffer and returns the result of `#to_i` of the contained `writable`.
|
41
|
+
# Primarily facilitates working with StreamCRC32 objects where you finish the
|
42
|
+
# computation by retrieving the CRC as an integer
|
43
|
+
#
|
44
|
+
# @return [Integer] the return value of `writable#to_i`
|
45
|
+
def to_i
|
46
|
+
flush!
|
47
|
+
@writable.to_i
|
48
|
+
end
|
49
|
+
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Layout/CommentIndentation, Metrics/LineLength, Metrics/AbcSize, Style/RedundantParentheses, Metrics/PerceivedComplexity, Layout/MultilineOperationIndentation, Layout/AlignParameters, Style/ConditionalAssignment, Layout/ExtraSpacing, Metrics/CyclomaticComplexity, Lint/UselessAssignment, Metrics/ParameterLists, Layout/LeadingCommentSpace, Naming/ConstantName
|
4
|
-
#
|
5
3
|
# A low-level ZIP file data writer. You can use it to write out various headers and central directory elements
|
6
4
|
# separately. The class handles the actual encoding of the data according to the ZIP format APPNOTE document.
|
7
5
|
#
|
@@ -29,7 +27,7 @@
|
|
29
27
|
class ZipTricks::ZipWriter
|
30
28
|
FOUR_BYTE_MAX_UINT = 0xFFFFFFFF
|
31
29
|
TWO_BYTE_MAX_UINT = 0xFFFF
|
32
|
-
ZIP_TRICKS_COMMENT = 'Written using ZipTricks
|
30
|
+
ZIP_TRICKS_COMMENT = 'Written using ZipTricks %<version>s' % {version: ZipTricks::VERSION}
|
33
31
|
VERSION_MADE_BY = 52
|
34
32
|
VERSION_NEEDED_TO_EXTRACT = 20
|
35
33
|
VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45
|
@@ -41,13 +39,13 @@ class ZipTricks::ZipWriter
|
|
41
39
|
# We snatch the incantations from Rubyzip for this.
|
42
40
|
unix_perms = 0o644
|
43
41
|
file_type_file = 0o10
|
44
|
-
|
42
|
+
(file_type_file << 12 | (unix_perms & 0o7777)) << 16
|
45
43
|
end
|
46
44
|
EMPTY_DIRECTORY_EXTERNAL_ATTRS = begin
|
47
45
|
# Applies permissions to an empty directory.
|
48
46
|
unix_perms = 0o755
|
49
47
|
file_type_dir = 0o04
|
50
|
-
|
48
|
+
(file_type_dir << 12 | (unix_perms & 0o7777)) << 16
|
51
49
|
end
|
52
50
|
MADE_BY_SIGNATURE = begin
|
53
51
|
# A combination of the VERSION_MADE_BY low byte and the OS type high byte
|
@@ -55,16 +53,23 @@ class ZipTricks::ZipWriter
|
|
55
53
|
[VERSION_MADE_BY, os_type].pack('CC')
|
56
54
|
end
|
57
55
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
private_constant :FOUR_BYTE_MAX_UINT,
|
65
|
-
|
66
|
-
|
67
|
-
|
56
|
+
C_UINT4 = 'V' # Encode a 4-byte unsigned little-endian uint
|
57
|
+
C_UINT2 = 'v' # Encode a 2-byte unsigned little-endian uint
|
58
|
+
C_UINT8 = 'Q<' # Encode an 8-byte unsigned little-endian uint
|
59
|
+
C_CHAR = 'C' # For bit-encoded strings
|
60
|
+
C_INT4 = 'N' # Encode a 4-byte signed little-endian int
|
61
|
+
|
62
|
+
private_constant :FOUR_BYTE_MAX_UINT,
|
63
|
+
:TWO_BYTE_MAX_UINT,
|
64
|
+
:VERSION_MADE_BY,
|
65
|
+
:VERSION_NEEDED_TO_EXTRACT,
|
66
|
+
:VERSION_NEEDED_TO_EXTRACT_ZIP64,
|
67
|
+
:DEFAULT_EXTERNAL_ATTRS,
|
68
|
+
:MADE_BY_SIGNATURE,
|
69
|
+
:C_UINT4,
|
70
|
+
:C_UINT2,
|
71
|
+
:C_UINT8,
|
72
|
+
:ZIP_TRICKS_COMMENT
|
68
73
|
|
69
74
|
# Writes the local file header, that precedes the actual file _data_.
|
70
75
|
#
|
@@ -80,29 +85,29 @@ class ZipTricks::ZipWriter
|
|
80
85
|
def write_local_file_header(io:, filename:, compressed_size:, uncompressed_size:, crc32:, gp_flags:, mtime:, storage_mode:)
|
81
86
|
requires_zip64 = (compressed_size > FOUR_BYTE_MAX_UINT || uncompressed_size > FOUR_BYTE_MAX_UINT)
|
82
87
|
|
83
|
-
io << [0x04034b50].pack(
|
84
|
-
if requires_zip64
|
85
|
-
|
88
|
+
io << [0x04034b50].pack(C_UINT4) # local file header signature 4 bytes (0x04034b50)
|
89
|
+
io << if requires_zip64 # version needed to extract 2 bytes
|
90
|
+
[VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_UINT2)
|
86
91
|
else
|
87
|
-
|
92
|
+
[VERSION_NEEDED_TO_EXTRACT].pack(C_UINT2)
|
88
93
|
end
|
89
94
|
|
90
|
-
io << [gp_flags].pack(
|
91
|
-
io << [storage_mode].pack(
|
92
|
-
io << [to_binary_dos_time(mtime)].pack(
|
93
|
-
io << [to_binary_dos_date(mtime)].pack(
|
94
|
-
io << [crc32].pack(
|
95
|
+
io << [gp_flags].pack(C_UINT2) # general purpose bit flag 2 bytes
|
96
|
+
io << [storage_mode].pack(C_UINT2) # compression method 2 bytes
|
97
|
+
io << [to_binary_dos_time(mtime)].pack(C_UINT2) # last mod file time 2 bytes
|
98
|
+
io << [to_binary_dos_date(mtime)].pack(C_UINT2) # last mod file date 2 bytes
|
99
|
+
io << [crc32].pack(C_UINT4) # crc-32 4 bytes
|
95
100
|
|
96
101
|
if requires_zip64
|
97
|
-
io << [FOUR_BYTE_MAX_UINT].pack(
|
98
|
-
io << [FOUR_BYTE_MAX_UINT].pack(
|
102
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4) # compressed size 4 bytes
|
103
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4) # uncompressed size 4 bytes
|
99
104
|
else
|
100
|
-
io << [compressed_size].pack(
|
101
|
-
io << [uncompressed_size].pack(
|
105
|
+
io << [compressed_size].pack(C_UINT4) # compressed size 4 bytes
|
106
|
+
io << [uncompressed_size].pack(C_UINT4) # uncompressed size 4 bytes
|
102
107
|
end
|
103
108
|
|
104
109
|
# Filename should not be longer than 0xFFFF otherwise this wont fit here
|
105
|
-
io << [filename.bytesize].pack(
|
110
|
+
io << [filename.bytesize].pack(C_UINT2) # file name length 2 bytes
|
106
111
|
|
107
112
|
extra_fields = StringIO.new
|
108
113
|
|
@@ -115,7 +120,7 @@ class ZipTricks::ZipWriter
|
|
115
120
|
end
|
116
121
|
extra_fields << timestamp_extra(mtime)
|
117
122
|
|
118
|
-
io << [extra_fields.size].pack(
|
123
|
+
io << [extra_fields.size].pack(C_UINT2) # extra field length 2 bytes
|
119
124
|
|
120
125
|
io << filename # file name (variable size)
|
121
126
|
io << extra_fields.string
|
@@ -144,32 +149,32 @@ class ZipTricks::ZipWriter
|
|
144
149
|
# At this point if the header begins somewhere beyound 0xFFFFFFFF we _have_ to record the offset
|
145
150
|
# of the local file header as a zip64 extra field, so we give up, give in, you loose, love will always win...
|
146
151
|
add_zip64 = (local_file_header_location > FOUR_BYTE_MAX_UINT) ||
|
147
|
-
|
152
|
+
(compressed_size > FOUR_BYTE_MAX_UINT) || (uncompressed_size > FOUR_BYTE_MAX_UINT)
|
148
153
|
|
149
|
-
io << [0x02014b50].pack(
|
154
|
+
io << [0x02014b50].pack(C_UINT4) # central file header signature 4 bytes (0x02014b50)
|
150
155
|
io << MADE_BY_SIGNATURE # version made by 2 bytes
|
151
|
-
if add_zip64
|
152
|
-
|
156
|
+
io << if add_zip64
|
157
|
+
[VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_UINT2) # version needed to extract 2 bytes
|
153
158
|
else
|
154
|
-
|
159
|
+
[VERSION_NEEDED_TO_EXTRACT].pack(C_UINT2) # version needed to extract 2 bytes
|
155
160
|
end
|
156
161
|
|
157
|
-
io << [gp_flags].pack(
|
158
|
-
io << [storage_mode].pack(
|
159
|
-
io << [to_binary_dos_time(mtime)].pack(
|
160
|
-
io << [to_binary_dos_date(mtime)].pack(
|
161
|
-
io << [crc32].pack(
|
162
|
+
io << [gp_flags].pack(C_UINT2) # general purpose bit flag 2 bytes
|
163
|
+
io << [storage_mode].pack(C_UINT2) # compression method 2 bytes
|
164
|
+
io << [to_binary_dos_time(mtime)].pack(C_UINT2) # last mod file time 2 bytes
|
165
|
+
io << [to_binary_dos_date(mtime)].pack(C_UINT2) # last mod file date 2 bytes
|
166
|
+
io << [crc32].pack(C_UINT4) # crc-32 4 bytes
|
162
167
|
|
163
168
|
if add_zip64
|
164
|
-
io << [FOUR_BYTE_MAX_UINT].pack(
|
165
|
-
io << [FOUR_BYTE_MAX_UINT].pack(
|
169
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4) # compressed size 4 bytes
|
170
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4) # uncompressed size 4 bytes
|
166
171
|
else
|
167
|
-
io << [compressed_size].pack(
|
168
|
-
io << [uncompressed_size].pack(
|
172
|
+
io << [compressed_size].pack(C_UINT4) # compressed size 4 bytes
|
173
|
+
io << [uncompressed_size].pack(C_UINT4) # uncompressed size 4 bytes
|
169
174
|
end
|
170
175
|
|
171
176
|
# Filename should not be longer than 0xFFFF otherwise this wont fit here
|
172
|
-
io << [filename.bytesize].pack(
|
177
|
+
io << [filename.bytesize].pack(C_UINT2) # file name length 2 bytes
|
173
178
|
|
174
179
|
extra_fields = StringIO.new
|
175
180
|
if add_zip64
|
@@ -179,36 +184,37 @@ class ZipTricks::ZipWriter
|
|
179
184
|
end
|
180
185
|
extra_fields << timestamp_extra(mtime)
|
181
186
|
|
182
|
-
io << [extra_fields.size].pack(
|
187
|
+
io << [extra_fields.size].pack(C_UINT2) # extra field length 2 bytes
|
183
188
|
|
184
|
-
io << [0].pack(
|
189
|
+
io << [0].pack(C_UINT2) # file comment length 2 bytes
|
185
190
|
|
186
191
|
# For The Unarchiver < 3.11.1 this field has to be set to the overflow value if zip64 is used
|
187
192
|
# because otherwise it does not properly advance the pointer when reading the Zip64 extra field
|
188
193
|
# https://bitbucket.org/WAHa_06x36/theunarchiver/pull-requests/2/bug-fix-for-zip64-extra-field-parser/diff
|
189
|
-
if add_zip64 # disk number start 2 bytes
|
190
|
-
|
194
|
+
io << if add_zip64 # disk number start 2 bytes
|
195
|
+
[TWO_BYTE_MAX_UINT].pack(C_UINT2)
|
191
196
|
else
|
192
|
-
|
193
|
-
|
194
|
-
io << [0].pack(
|
197
|
+
[0].pack(C_UINT2)
|
198
|
+
end
|
199
|
+
io << [0].pack(C_UINT2) # internal file attributes 2 bytes
|
195
200
|
|
196
201
|
# Because the add_empty_directory method will create a directory with a trailing "/",
|
197
202
|
# this check can be used to assign proper permissions to the created directory.
|
198
|
-
if filename.end_with?('/')
|
199
|
-
|
203
|
+
io << if filename.end_with?('/')
|
204
|
+
[EMPTY_DIRECTORY_EXTERNAL_ATTRS].pack(C_UINT4)
|
200
205
|
else
|
201
|
-
|
206
|
+
[DEFAULT_EXTERNAL_ATTRS].pack(C_UINT4) # external file attributes 4 bytes
|
202
207
|
end
|
203
208
|
|
204
|
-
if add_zip64
|
205
|
-
|
209
|
+
io << if add_zip64 # relative offset of local header 4 bytes
|
210
|
+
[FOUR_BYTE_MAX_UINT].pack(C_UINT4)
|
206
211
|
else
|
207
|
-
|
212
|
+
[local_file_header_location].pack(C_UINT4)
|
208
213
|
end
|
214
|
+
|
209
215
|
io << filename # file name (variable size)
|
210
216
|
io << extra_fields.string # extra field (variable size)
|
211
|
-
#(empty)
|
217
|
+
# (empty) # file comment (variable size)
|
212
218
|
end
|
213
219
|
|
214
220
|
# Writes the data descriptor following the file data for a file whose local file header
|
@@ -221,16 +227,16 @@ class ZipTricks::ZipWriter
|
|
221
227
|
# @param uncompressed_size[Fixnum] The size of the file once extracted
|
222
228
|
# @return [void]
|
223
229
|
def write_data_descriptor(io:, compressed_size:, uncompressed_size:, crc32:)
|
224
|
-
io << [0x08074b50].pack(
|
225
|
-
|
226
|
-
|
227
|
-
io << [crc32].pack(
|
230
|
+
io << [0x08074b50].pack(C_UINT4) # Although not originally assigned a signature, the value
|
231
|
+
# 0x08074b50 has commonly been adopted as a signature value
|
232
|
+
# for the data descriptor record.
|
233
|
+
io << [crc32].pack(C_UINT4) # crc-32 4 bytes
|
228
234
|
|
229
235
|
# If one of the sizes is above 0xFFFFFFF use ZIP64 lengths (8 bytes) instead. A good unarchiver
|
230
236
|
# will decide to unpack it as such if it finds the Zip64 extra for the file in the central directory.
|
231
237
|
# So also use the opportune moment to switch the entry to Zip64 if needed
|
232
238
|
requires_zip64 = (compressed_size > FOUR_BYTE_MAX_UINT || uncompressed_size > FOUR_BYTE_MAX_UINT)
|
233
|
-
pack_spec = requires_zip64 ?
|
239
|
+
pack_spec = requires_zip64 ? C_UINT8 : C_UINT4
|
234
240
|
|
235
241
|
io << [compressed_size].pack(pack_spec) # compressed size 4 bytes, or 8 bytes for ZIP64
|
236
242
|
io << [uncompressed_size].pack(pack_spec) # uncompressed size 4 bytes, or 8 bytes for ZIP64
|
@@ -248,77 +254,77 @@ class ZipTricks::ZipWriter
|
|
248
254
|
zip64_eocdr_offset = start_of_central_directory_location + central_directory_size
|
249
255
|
|
250
256
|
zip64_required = central_directory_size > FOUR_BYTE_MAX_UINT ||
|
251
|
-
|
252
|
-
|
253
|
-
|
257
|
+
start_of_central_directory_location > FOUR_BYTE_MAX_UINT ||
|
258
|
+
zip64_eocdr_offset > FOUR_BYTE_MAX_UINT ||
|
259
|
+
num_files_in_archive > TWO_BYTE_MAX_UINT
|
254
260
|
|
255
261
|
# Then, if zip64 is used
|
256
262
|
if zip64_required
|
257
263
|
# [zip64 end of central directory record]
|
258
|
-
|
259
|
-
io << [0x06064b50].pack(
|
260
|
-
io << [44].pack(
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
264
|
+
# zip64 end of central dir
|
265
|
+
io << [0x06064b50].pack(C_UINT4) # signature 4 bytes (0x06064b50)
|
266
|
+
io << [44].pack(C_UINT8) # size of zip64 end of central
|
267
|
+
# directory record 8 bytes
|
268
|
+
# (this is ex. the 12 bytes of the signature and the size value itself).
|
269
|
+
# Without the extensible data sector (which we are not using)
|
270
|
+
# it is always 44 bytes.
|
265
271
|
io << MADE_BY_SIGNATURE # version made by 2 bytes
|
266
|
-
io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(
|
267
|
-
io << [0].pack(
|
268
|
-
io << [0].pack(
|
269
|
-
|
270
|
-
io << [num_files_in_archive].pack(
|
271
|
-
|
272
|
-
io << [num_files_in_archive].pack(
|
273
|
-
|
274
|
-
io << [central_directory_size].pack(
|
275
|
-
|
276
|
-
|
277
|
-
io << [start_of_central_directory_location].pack(
|
278
|
-
|
272
|
+
io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_UINT2) # version needed to extract 2 bytes
|
273
|
+
io << [0].pack(C_UINT4) # number of this disk 4 bytes
|
274
|
+
io << [0].pack(C_UINT4) # number of the disk with the
|
275
|
+
# start of the central directory 4 bytes
|
276
|
+
io << [num_files_in_archive].pack(C_UINT8) # total number of entries in the
|
277
|
+
# central directory on this disk 8 bytes
|
278
|
+
io << [num_files_in_archive].pack(C_UINT8) # total number of entries in the
|
279
|
+
# central directory 8 bytes
|
280
|
+
io << [central_directory_size].pack(C_UINT8) # size of the central directory 8 bytes
|
281
|
+
# offset of start of central
|
282
|
+
# directory with respect to
|
283
|
+
io << [start_of_central_directory_location].pack(C_UINT8) # the starting disk number 8 bytes
|
284
|
+
# zip64 extensible data sector (variable size), blank for us
|
279
285
|
|
280
286
|
# [zip64 end of central directory locator]
|
281
|
-
io << [0x07064b50].pack(
|
282
|
-
|
283
|
-
io << [0].pack(
|
284
|
-
|
285
|
-
|
286
|
-
io << [zip64_eocdr_offset].pack(
|
287
|
-
|
288
|
-
|
289
|
-
io << [1].pack(
|
287
|
+
io << [0x07064b50].pack(C_UINT4) # zip64 end of central dir locator
|
288
|
+
# signature 4 bytes (0x07064b50)
|
289
|
+
io << [0].pack(C_UINT4) # number of the disk with the
|
290
|
+
# start of the zip64 end of
|
291
|
+
# central directory 4 bytes
|
292
|
+
io << [zip64_eocdr_offset].pack(C_UINT8) # relative offset of the zip64
|
293
|
+
# end of central directory record 8 bytes
|
294
|
+
# (note: "relative" is actually "from the start of the file")
|
295
|
+
io << [1].pack(C_UINT4) # total number of disks 4 bytes
|
290
296
|
end
|
291
297
|
|
292
298
|
# Then the end of central directory record:
|
293
|
-
io << [0x06054b50].pack(
|
294
|
-
io << [0].pack(
|
295
|
-
io << [0].pack(
|
296
|
-
|
299
|
+
io << [0x06054b50].pack(C_UINT4) # end of central dir signature 4 bytes (0x06054b50)
|
300
|
+
io << [0].pack(C_UINT2) # number of this disk 2 bytes
|
301
|
+
io << [0].pack(C_UINT2) # number of the disk with the
|
302
|
+
# start of the central directory 2 bytes
|
297
303
|
|
298
304
|
if zip64_required # the number of entries will be read from the zip64 part of the central directory
|
299
|
-
io << [TWO_BYTE_MAX_UINT].pack(
|
300
|
-
|
301
|
-
io << [TWO_BYTE_MAX_UINT].pack(
|
302
|
-
|
305
|
+
io << [TWO_BYTE_MAX_UINT].pack(C_UINT2) # total number of entries in the
|
306
|
+
# central directory on this disk 2 bytes
|
307
|
+
io << [TWO_BYTE_MAX_UINT].pack(C_UINT2) # total number of entries in
|
308
|
+
# the central directory 2 bytes
|
303
309
|
else
|
304
|
-
io << [num_files_in_archive].pack(
|
305
|
-
|
306
|
-
io << [num_files_in_archive].pack(
|
307
|
-
|
310
|
+
io << [num_files_in_archive].pack(C_UINT2) # total number of entries in the
|
311
|
+
# central directory on this disk 2 bytes
|
312
|
+
io << [num_files_in_archive].pack(C_UINT2) # total number of entries in
|
313
|
+
# the central directory 2 bytes
|
308
314
|
end
|
309
315
|
|
310
316
|
if zip64_required
|
311
|
-
io << [FOUR_BYTE_MAX_UINT].pack(
|
312
|
-
io << [FOUR_BYTE_MAX_UINT].pack(
|
313
|
-
|
314
|
-
|
317
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4) # size of the central directory 4 bytes
|
318
|
+
io << [FOUR_BYTE_MAX_UINT].pack(C_UINT4) # offset of start of central
|
319
|
+
# directory with respect to
|
320
|
+
# the starting disk number 4 bytes
|
315
321
|
else
|
316
|
-
io << [central_directory_size].pack(
|
317
|
-
io << [start_of_central_directory_location].pack(
|
318
|
-
|
319
|
-
|
322
|
+
io << [central_directory_size].pack(C_UINT4) # size of the central directory 4 bytes
|
323
|
+
io << [start_of_central_directory_location].pack(C_UINT4) # offset of start of central
|
324
|
+
# directory with respect to
|
325
|
+
# the starting disk number 4 bytes
|
320
326
|
end
|
321
|
-
io << [comment.bytesize].pack(
|
327
|
+
io << [comment.bytesize].pack(C_UINT2) # .ZIP file comment length 2 bytes
|
322
328
|
io << comment # .ZIP file comment (variable size)
|
323
329
|
end
|
324
330
|
|
@@ -331,10 +337,10 @@ class ZipTricks::ZipWriter
|
|
331
337
|
# @return [String]
|
332
338
|
def zip_64_extra_for_local_file_header(compressed_size:, uncompressed_size:)
|
333
339
|
data_and_packspecs = [
|
334
|
-
0x0001,
|
335
|
-
16,
|
336
|
-
uncompressed_size,
|
337
|
-
compressed_size,
|
340
|
+
0x0001, C_UINT2, # 2 bytes Tag for this "extra" block type
|
341
|
+
16, C_UINT2, # 2 bytes Size of this "extra" block. For us it will always be 16 (2x8)
|
342
|
+
uncompressed_size, C_UINT8, # 8 bytes Original uncompressed file size
|
343
|
+
compressed_size, C_UINT8, # 8 bytes Size of compressed data
|
338
344
|
]
|
339
345
|
pack_array(data_and_packspecs)
|
340
346
|
end
|
@@ -374,10 +380,10 @@ class ZipTricks::ZipWriter
|
|
374
380
|
# bits 3-7 reserved for additional timestamps; not set
|
375
381
|
flags = 0b10000000 # Set bit 1 only to indicate only mtime is present
|
376
382
|
data_and_packspecs = [
|
377
|
-
0x5455,
|
378
|
-
(1 + 4),
|
379
|
-
flags,
|
380
|
-
mtime.utc.to_i,
|
383
|
+
0x5455, C_UINT2, # tag for this extra block type ("UT")
|
384
|
+
(1 + 4), C_UINT2, # the size of this block (1 byte used for the Flag + 1 long used for the timestamp)
|
385
|
+
flags, C_CHAR, # encode a single byte
|
386
|
+
mtime.utc.to_i, C_INT4, # Use a signed long, not the unsigned one used by the rest of the ZIP spec.
|
381
387
|
]
|
382
388
|
pack_array(data_and_packspecs)
|
383
389
|
end
|
@@ -391,12 +397,12 @@ class ZipTricks::ZipWriter
|
|
391
397
|
# @return [String]
|
392
398
|
def zip_64_extra_for_central_directory_file_header(compressed_size:, uncompressed_size:, local_file_header_location:)
|
393
399
|
data_and_packspecs = [
|
394
|
-
0x0001,
|
395
|
-
28,
|
396
|
-
uncompressed_size,
|
397
|
-
compressed_size,
|
398
|
-
local_file_header_location,
|
399
|
-
0,
|
400
|
+
0x0001, C_UINT2, # 2 bytes Tag for this "extra" block type
|
401
|
+
28, C_UINT2, # 2 bytes Size of this "extra" block. For us it will always be 28
|
402
|
+
uncompressed_size, C_UINT8, # 8 bytes Original uncompressed file size
|
403
|
+
compressed_size, C_UINT8, # 8 bytes Size of compressed data
|
404
|
+
local_file_header_location, C_UINT8, # 8 bytes Offset of local header record
|
405
|
+
0, C_UINT4, # 4 bytes Number of the disk on which this file starts
|
400
406
|
]
|
401
407
|
pack_array(data_and_packspecs)
|
402
408
|
end
|
@@ -406,7 +412,7 @@ class ZipTricks::ZipWriter
|
|
406
412
|
end
|
407
413
|
|
408
414
|
def to_binary_dos_date(t)
|
409
|
-
|
415
|
+
t.day + (t.month << 5) + ((t.year - 1980) << 9)
|
410
416
|
end
|
411
417
|
|
412
418
|
# Unzips a given array of tuples of "numeric value, pack specifier" and then packs all the odd
|