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.
@@ -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, crc32: crc32,
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(filename:,
392
- crc32:,
393
- storage_mode:,
394
- compressed_size:,
395
- uncompressed_size:,
396
- use_data_descriptor:)
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
- mtime = Time.now.utc,
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
- @io = io
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
- def finish
19
- @io << @deflater.finish until @deflater.finished?
20
- {crc32: @crc.to_i, compressed_size: @io.tell - @started_at, uncompressed_size: @uncompressed_size}
21
- end
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
- @io << @deflater.deflate(data)
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
- @io << @deflater.flush
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
- # Rubocop: convention: Missing top-level class documentation comment.
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
- @uncompressed_size = 0
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
- size = @io.tell - @started_at
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZipTricks
4
- VERSION = '4.5.2'
4
+ VERSION = '4.6.0'
5
5
  end
@@ -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 %s' % ZipTricks::VERSION
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
- external_attrs = (file_type_file << 12 | (unix_perms & 0o7777)) << 16
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
- external_attrs = (file_type_dir << 12 | (unix_perms & 0o7777)) << 16
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
- C_V = 'V' # Encode a 4-byte unsigned little-endian uint
59
- C_v = 'v' # Encode a 2-byte unsigned little-endian uint
60
- C_Qe = 'Q<' # Encode an 8-byte unsigned little-endian uint
61
- C_C = 'C' # For bit-encoded strings
62
- C_N = 'N' # Encode a 4-byte signed little-endian int
63
-
64
- private_constant :FOUR_BYTE_MAX_UINT, :TWO_BYTE_MAX_UINT,
65
- :VERSION_MADE_BY, :VERSION_NEEDED_TO_EXTRACT, :VERSION_NEEDED_TO_EXTRACT_ZIP64,
66
- :DEFAULT_EXTERNAL_ATTRS, :MADE_BY_SIGNATURE,
67
- :C_V, :C_v, :C_Qe, :ZIP_TRICKS_COMMENT
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(C_V) # local file header signature 4 bytes (0x04034b50)
84
- if requires_zip64 # version needed to extract 2 bytes
85
- io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_v)
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
- io << [VERSION_NEEDED_TO_EXTRACT].pack(C_v)
92
+ [VERSION_NEEDED_TO_EXTRACT].pack(C_UINT2)
88
93
  end
89
94
 
90
- io << [gp_flags].pack(C_v) # general purpose bit flag 2 bytes
91
- io << [storage_mode].pack(C_v) # compression method 2 bytes
92
- io << [to_binary_dos_time(mtime)].pack(C_v) # last mod file time 2 bytes
93
- io << [to_binary_dos_date(mtime)].pack(C_v) # last mod file date 2 bytes
94
- io << [crc32].pack(C_V) # crc-32 4 bytes
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(C_V) # compressed size 4 bytes
98
- io << [FOUR_BYTE_MAX_UINT].pack(C_V) # uncompressed size 4 bytes
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(C_V) # compressed size 4 bytes
101
- io << [uncompressed_size].pack(C_V) # uncompressed size 4 bytes
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(C_v) # file name length 2 bytes
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(C_v) # extra field length 2 bytes
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
- (compressed_size > FOUR_BYTE_MAX_UINT) || (uncompressed_size > FOUR_BYTE_MAX_UINT)
152
+ (compressed_size > FOUR_BYTE_MAX_UINT) || (uncompressed_size > FOUR_BYTE_MAX_UINT)
148
153
 
149
- io << [0x02014b50].pack(C_V) # central file header signature 4 bytes (0x02014b50)
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
- io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_v) # version needed to extract 2 bytes
156
+ io << if add_zip64
157
+ [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_UINT2) # version needed to extract 2 bytes
153
158
  else
154
- io << [VERSION_NEEDED_TO_EXTRACT].pack(C_v) # version needed to extract 2 bytes
159
+ [VERSION_NEEDED_TO_EXTRACT].pack(C_UINT2) # version needed to extract 2 bytes
155
160
  end
156
161
 
157
- io << [gp_flags].pack(C_v) # general purpose bit flag 2 bytes
158
- io << [storage_mode].pack(C_v) # compression method 2 bytes
159
- io << [to_binary_dos_time(mtime)].pack(C_v) # last mod file time 2 bytes
160
- io << [to_binary_dos_date(mtime)].pack(C_v) # last mod file date 2 bytes
161
- io << [crc32].pack(C_V) # crc-32 4 bytes
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(C_V) # compressed size 4 bytes
165
- io << [FOUR_BYTE_MAX_UINT].pack(C_V) # uncompressed size 4 bytes
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(C_V) # compressed size 4 bytes
168
- io << [uncompressed_size].pack(C_V) # uncompressed size 4 bytes
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(C_v) # file name length 2 bytes
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(C_v) # extra field length 2 bytes
187
+ io << [extra_fields.size].pack(C_UINT2) # extra field length 2 bytes
183
188
 
184
- io << [0].pack(C_v) # file comment length 2 bytes
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
- io << [TWO_BYTE_MAX_UINT].pack(C_v)
194
+ io << if add_zip64 # disk number start 2 bytes
195
+ [TWO_BYTE_MAX_UINT].pack(C_UINT2)
191
196
  else
192
- io << [0].pack(C_v)
193
- end
194
- io << [0].pack(C_v) # internal file attributes 2 bytes
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
- io << [EMPTY_DIRECTORY_EXTERNAL_ATTRS].pack(C_V)
203
+ io << if filename.end_with?('/')
204
+ [EMPTY_DIRECTORY_EXTERNAL_ATTRS].pack(C_UINT4)
200
205
  else
201
- io << [DEFAULT_EXTERNAL_ATTRS].pack(C_V) # external file attributes 4 bytes
206
+ [DEFAULT_EXTERNAL_ATTRS].pack(C_UINT4) # external file attributes 4 bytes
202
207
  end
203
208
 
204
- if add_zip64 # relative offset of local header 4 bytes
205
- io << [FOUR_BYTE_MAX_UINT].pack(C_V)
209
+ io << if add_zip64 # relative offset of local header 4 bytes
210
+ [FOUR_BYTE_MAX_UINT].pack(C_UINT4)
206
211
  else
207
- io << [local_file_header_location].pack(C_V)
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) # file comment (variable size)
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(C_V) # Although not originally assigned a signature, the value
225
- # 0x08074b50 has commonly been adopted as a signature value
226
- # for the data descriptor record.
227
- io << [crc32].pack(C_V) # crc-32 4 bytes
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 ? C_Qe : C_V
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
- start_of_central_directory_location > FOUR_BYTE_MAX_UINT ||
252
- zip64_eocdr_offset > FOUR_BYTE_MAX_UINT ||
253
- num_files_in_archive > TWO_BYTE_MAX_UINT
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
- # zip64 end of central dir
259
- io << [0x06064b50].pack(C_V) # signature 4 bytes (0x06064b50)
260
- io << [44].pack(C_Qe) # size of zip64 end of central
261
- # directory record 8 bytes
262
- # (this is ex. the 12 bytes of the signature and the size value itself).
263
- # Without the extensible data sector (which we are not using)
264
- # it is always 44 bytes.
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(C_v) # version needed to extract 2 bytes
267
- io << [0].pack(C_V) # number of this disk 4 bytes
268
- io << [0].pack(C_V) # number of the disk with the
269
- # start of the central directory 4 bytes
270
- io << [num_files_in_archive].pack(C_Qe) # total number of entries in the
271
- # central directory on this disk 8 bytes
272
- io << [num_files_in_archive].pack(C_Qe) # total number of entries in the
273
- # central directory 8 bytes
274
- io << [central_directory_size].pack(C_Qe) # size of the central directory 8 bytes
275
- # offset of start of central
276
- # directory with respect to
277
- io << [start_of_central_directory_location].pack(C_Qe) # the starting disk number 8 bytes
278
- # zip64 extensible data sector (variable size), blank for us
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(C_V) # zip64 end of central dir locator
282
- # signature 4 bytes (0x07064b50)
283
- io << [0].pack(C_V) # number of the disk with the
284
- # start of the zip64 end of
285
- # central directory 4 bytes
286
- io << [zip64_eocdr_offset].pack(C_Qe) # relative offset of the zip64
287
- # end of central directory record 8 bytes
288
- # (note: "relative" is actually "from the start of the file")
289
- io << [1].pack(C_V) # total number of disks 4 bytes
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(C_V) # end of central dir signature 4 bytes (0x06054b50)
294
- io << [0].pack(C_v) # number of this disk 2 bytes
295
- io << [0].pack(C_v) # number of the disk with the
296
- # start of the central directory 2 bytes
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(C_v) # total number of entries in the
300
- # central directory on this disk 2 bytes
301
- io << [TWO_BYTE_MAX_UINT].pack(C_v) # total number of entries in
302
- # the central directory 2 bytes
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(C_v) # total number of entries in the
305
- # central directory on this disk 2 bytes
306
- io << [num_files_in_archive].pack(C_v) # total number of entries in
307
- # the central directory 2 bytes
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(C_V) # size of the central directory 4 bytes
312
- io << [FOUR_BYTE_MAX_UINT].pack(C_V) # offset of start of central
313
- # directory with respect to
314
- # the starting disk number 4 bytes
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(C_V) # size of the central directory 4 bytes
317
- io << [start_of_central_directory_location].pack(C_V) # offset of start of central
318
- # directory with respect to
319
- # the starting disk number 4 bytes
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(C_v) # .ZIP file comment length 2 bytes
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, C_v, # 2 bytes Tag for this "extra" block type
335
- 16, C_v, # 2 bytes Size of this "extra" block. For us it will always be 16 (2x8)
336
- uncompressed_size, C_Qe, # 8 bytes Original uncompressed file size
337
- compressed_size, C_Qe, # 8 bytes Size of compressed data
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, C_v, # tag for this extra block type ("UT")
378
- (1 + 4), C_v, # the size of this block (1 byte used for the Flag + 1 long used for the timestamp)
379
- flags, C_C, # encode a single byte
380
- mtime.utc.to_i, C_N, # Use a signed long, not the unsigned one used by the rest of the ZIP spec.
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, C_v, # 2 bytes Tag for this "extra" block type
395
- 28, C_v, # 2 bytes Size of this "extra" block. For us it will always be 28
396
- uncompressed_size, C_Qe, # 8 bytes Original uncompressed file size
397
- compressed_size, C_Qe, # 8 bytes Size of compressed data
398
- local_file_header_location, C_Qe, # 8 bytes Offset of local header record
399
- 0, C_V, # 4 bytes Number of the disk on which this file starts
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
- (t.day) + (t.month << 5) + ((t.year - 1980) << 9)
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