zip_tricks 4.5.2 → 4.6.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 +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
|