rubyzip 1.3.0 → 3.4.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.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +516 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +213 -51
  5. data/Rakefile +17 -12
  6. data/lib/rubyzip.rb +3 -0
  7. data/lib/zip/central_directory.rb +196 -123
  8. data/lib/zip/compressor.rb +3 -1
  9. data/lib/zip/constants.rb +81 -21
  10. data/lib/zip/crypto/aes_encryption.rb +120 -0
  11. data/lib/zip/crypto/decrypted_io.rb +46 -0
  12. data/lib/zip/crypto/encryption.rb +4 -2
  13. data/lib/zip/crypto/null_encryption.rb +5 -13
  14. data/lib/zip/crypto/traditional_encryption.rb +22 -16
  15. data/lib/zip/decompressor.rb +21 -2
  16. data/lib/zip/deflater.rb +12 -8
  17. data/lib/zip/dirtyable.rb +32 -0
  18. data/lib/zip/dos_time.rb +65 -13
  19. data/lib/zip/entry.rb +433 -249
  20. data/lib/zip/entry_set.rb +13 -9
  21. data/lib/zip/errors.rb +152 -15
  22. data/lib/zip/extra_field/aes.rb +50 -0
  23. data/lib/zip/extra_field/generic.rb +18 -18
  24. data/lib/zip/extra_field/ntfs.rb +10 -4
  25. data/lib/zip/extra_field/old_unix.rb +6 -2
  26. data/lib/zip/extra_field/universal_time.rb +45 -13
  27. data/lib/zip/extra_field/unix.rb +7 -3
  28. data/lib/zip/extra_field/unknown.rb +35 -0
  29. data/lib/zip/extra_field/zip64.rb +23 -7
  30. data/lib/zip/extra_field.rb +31 -27
  31. data/lib/zip/file.rb +225 -241
  32. data/lib/zip/file_split.rb +91 -0
  33. data/lib/zip/filesystem/dir.rb +86 -0
  34. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  35. data/lib/zip/filesystem/file.rb +263 -0
  36. data/lib/zip/filesystem/file_stat.rb +110 -0
  37. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  38. data/lib/zip/filesystem.rb +32 -585
  39. data/lib/zip/inflater.rb +28 -37
  40. data/lib/zip/input_stream.rb +113 -54
  41. data/lib/zip/ioextras/abstract_input_stream.rb +154 -55
  42. data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
  43. data/lib/zip/ioextras.rb +8 -8
  44. data/lib/zip/null_compressor.rb +3 -1
  45. data/lib/zip/null_decompressor.rb +7 -12
  46. data/lib/zip/null_input_stream.rb +3 -1
  47. data/lib/zip/output_stream.rb +69 -50
  48. data/lib/zip/pass_thru_compressor.rb +5 -3
  49. data/lib/zip/pass_thru_decompressor.rb +17 -23
  50. data/lib/zip/streamable_directory.rb +6 -4
  51. data/lib/zip/streamable_stream.rb +10 -11
  52. data/lib/zip/version.rb +4 -1
  53. data/lib/zip.rb +32 -7
  54. data/rubyzip.gemspec +39 -0
  55. data/samples/example.rb +9 -4
  56. data/samples/example_filesystem.rb +4 -3
  57. data/samples/example_recursive.rb +3 -1
  58. data/samples/gtk_ruby_zip.rb +23 -21
  59. data/samples/qtzip.rb +13 -12
  60. data/samples/write_simple.rb +3 -4
  61. data/samples/zipfind.rb +24 -22
  62. metadata +85 -177
  63. data/TODO +0 -15
  64. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
  65. data/test/basic_zip_file_test.rb +0 -60
  66. data/test/case_sensitivity_test.rb +0 -69
  67. data/test/central_directory_entry_test.rb +0 -69
  68. data/test/central_directory_test.rb +0 -100
  69. data/test/crypto/null_encryption_test.rb +0 -57
  70. data/test/crypto/traditional_encryption_test.rb +0 -80
  71. data/test/data/WarnInvalidDate.zip +0 -0
  72. data/test/data/file1.txt +0 -46
  73. data/test/data/file1.txt.deflatedData +0 -0
  74. data/test/data/file2.txt +0 -1504
  75. data/test/data/globTest/foo/bar/baz/foo.txt +0 -0
  76. data/test/data/globTest/foo.txt +0 -0
  77. data/test/data/globTest/food.txt +0 -0
  78. data/test/data/globTest.zip +0 -0
  79. data/test/data/gpbit3stored.zip +0 -0
  80. data/test/data/mimetype +0 -1
  81. data/test/data/notzippedruby.rb +0 -7
  82. data/test/data/ntfs.zip +0 -0
  83. data/test/data/oddExtraField.zip +0 -0
  84. data/test/data/path_traversal/Makefile +0 -10
  85. data/test/data/path_traversal/jwilk/README.md +0 -5
  86. data/test/data/path_traversal/jwilk/absolute1.zip +0 -0
  87. data/test/data/path_traversal/jwilk/absolute2.zip +0 -0
  88. data/test/data/path_traversal/jwilk/dirsymlink.zip +0 -0
  89. data/test/data/path_traversal/jwilk/dirsymlink2a.zip +0 -0
  90. data/test/data/path_traversal/jwilk/dirsymlink2b.zip +0 -0
  91. data/test/data/path_traversal/jwilk/relative0.zip +0 -0
  92. data/test/data/path_traversal/jwilk/relative2.zip +0 -0
  93. data/test/data/path_traversal/jwilk/symlink.zip +0 -0
  94. data/test/data/path_traversal/relative1.zip +0 -0
  95. data/test/data/path_traversal/tilde.zip +0 -0
  96. data/test/data/path_traversal/tuzovakaoff/README.md +0 -3
  97. data/test/data/path_traversal/tuzovakaoff/absolutepath.zip +0 -0
  98. data/test/data/path_traversal/tuzovakaoff/symlink.zip +0 -0
  99. data/test/data/rubycode.zip +0 -0
  100. data/test/data/rubycode2.zip +0 -0
  101. data/test/data/test.xls +0 -0
  102. data/test/data/testDirectory.bin +0 -0
  103. data/test/data/zip64-sample.zip +0 -0
  104. data/test/data/zipWithDirs.zip +0 -0
  105. data/test/data/zipWithEncryption.zip +0 -0
  106. data/test/deflater_test.rb +0 -65
  107. data/test/encryption_test.rb +0 -42
  108. data/test/entry_set_test.rb +0 -163
  109. data/test/entry_test.rb +0 -154
  110. data/test/errors_test.rb +0 -35
  111. data/test/extra_field_test.rb +0 -76
  112. data/test/file_extract_directory_test.rb +0 -54
  113. data/test/file_extract_test.rb +0 -145
  114. data/test/file_permissions_test.rb +0 -65
  115. data/test/file_split_test.rb +0 -57
  116. data/test/file_test.rb +0 -666
  117. data/test/filesystem/dir_iterator_test.rb +0 -58
  118. data/test/filesystem/directory_test.rb +0 -139
  119. data/test/filesystem/file_mutating_test.rb +0 -87
  120. data/test/filesystem/file_nonmutating_test.rb +0 -508
  121. data/test/filesystem/file_stat_test.rb +0 -64
  122. data/test/gentestfiles.rb +0 -126
  123. data/test/inflater_test.rb +0 -14
  124. data/test/input_stream_test.rb +0 -182
  125. data/test/ioextras/abstract_input_stream_test.rb +0 -102
  126. data/test/ioextras/abstract_output_stream_test.rb +0 -106
  127. data/test/ioextras/fake_io_test.rb +0 -18
  128. data/test/local_entry_test.rb +0 -154
  129. data/test/output_stream_test.rb +0 -128
  130. data/test/pass_thru_compressor_test.rb +0 -30
  131. data/test/pass_thru_decompressor_test.rb +0 -14
  132. data/test/path_traversal_test.rb +0 -141
  133. data/test/samples/example_recursive_test.rb +0 -37
  134. data/test/settings_test.rb +0 -95
  135. data/test/test_helper.rb +0 -234
  136. data/test/unicode_file_names_and_comments_test.rb +0 -62
  137. data/test/zip64_full_test.rb +0 -51
  138. data/test/zip64_support_test.rb +0 -14
data/lib/zip/entry.rb CHANGED
@@ -1,21 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
4
+
5
+ require_relative 'constants'
6
+ require_relative 'dirtyable'
7
+
2
8
  module Zip
9
+ # Zip::Entry represents an entry in a Zip archive.
3
10
  class Entry
4
- STORED = 0
5
- DEFLATED = 8
11
+ include Dirtyable
12
+
13
+ # Constant used to specify that the entry is stored (i.e., not compressed).
14
+ STORED = ::Zip::COMPRESSION_METHOD_STORE
15
+
16
+ # Constant used to specify that the entry is deflated (i.e., compressed).
17
+ DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE
18
+
6
19
  # Language encoding flag (EFS) bit
7
- EFS = 0b100000000000
8
-
9
- attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
10
- :name, :size, :local_header_offset, :zipfile, :fstype, :external_file_attributes,
11
- :internal_file_attributes,
12
- :gp_flags, :header_signature, :follow_symlinks,
13
- :restore_times, :restore_permissions, :restore_ownership,
14
- :unix_uid, :unix_gid, :unix_perms,
15
- :dirty
16
- attr_reader :ftype, :filepath # :nodoc:
17
-
18
- def set_default_vars_values
20
+ EFS = 0b100000000000 # :nodoc:
21
+
22
+ # Compression level flags (used as part of the gp flags).
23
+ COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110 # :nodoc:
24
+ COMPRESSION_LEVEL_FAST_GPFLAG = 0b100 # :nodoc:
25
+ COMPRESSION_LEVEL_MAX_GPFLAG = 0b010 # :nodoc:
26
+
27
+ attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
28
+ :restore_ownership, :restore_permissions, :restore_times,
29
+ :unix_gid, :unix_perms, :unix_uid
30
+
31
+ attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
32
+ :internal_file_attributes, :local_header_offset # :nodoc:
33
+
34
+ attr_reader :extra, :compression_level, :filepath # :nodoc:
35
+
36
+ attr_writer :size # :nodoc:
37
+
38
+ mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
39
+ :fstype=, :gp_flags=, :name=, :size=,
40
+ :unix_gid=, :unix_perms=, :unix_uid=
41
+
42
+ def set_default_vars_values # :nodoc:
19
43
  @local_header_offset = 0
20
44
  @local_header_size = nil # not known until local entry is created or read
21
45
  @internal_file_attributes = 1
@@ -34,169 +58,257 @@ module Zip
34
58
  end
35
59
  @follow_symlinks = false
36
60
 
37
- @restore_times = true
38
- @restore_permissions = false
39
- @restore_ownership = false
61
+ @restore_times = DEFAULT_RESTORE_OPTIONS[:restore_times]
62
+ @restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
63
+ @restore_ownership = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
40
64
  # BUG: need an extra field to support uid/gid's
41
65
  @unix_uid = nil
42
66
  @unix_gid = nil
43
67
  @unix_perms = nil
44
- # @posix_acl = nil
45
- # @ntfs_acl = nil
46
- @dirty = false
47
68
  end
48
69
 
49
- def check_name(name)
50
- return unless name.start_with?('/')
51
- raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
70
+ def check_name(name) # :nodoc:
71
+ raise EntryNameError, name if name.start_with?('/')
72
+ raise EntryNameError if name.length > 65_535
52
73
  end
53
74
 
54
- def initialize(*args)
55
- name = args[1] || ''
56
- check_name(name)
75
+ # Create a new Zip::Entry.
76
+ def initialize(
77
+ zipfile = '', name = '',
78
+ comment: '', size: nil, compressed_size: 0, crc: 0,
79
+ compression_method: DEFLATED,
80
+ compression_level: ::Zip.default_compression,
81
+ time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
82
+ )
83
+ super()
84
+ @name = name
85
+ check_name(@name)
57
86
 
58
87
  set_default_vars_values
59
88
  @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
60
89
 
61
- @zipfile = args[0] || ''
62
- @name = name
63
- @comment = args[2] || ''
64
- @extra = args[3] || ''
65
- @compressed_size = args[4] || 0
66
- @crc = args[5] || 0
67
- @compression_method = args[6] || ::Zip::Entry::DEFLATED
68
- @size = args[7] || 0
69
- @time = args[8] || ::Zip::DOSTime.now
90
+ @zipfile = zipfile
91
+ @comment = comment || ''
92
+ @compression_method = compression_method || DEFLATED
93
+ @compression_level = compression_level || ::Zip.default_compression
94
+ @compressed_size = compressed_size || 0
95
+ @crc = crc || 0
96
+ @size = size
97
+ @time = case time
98
+ when ::Zip::DOSTime
99
+ time
100
+ when Time
101
+ ::Zip::DOSTime.from_time(time)
102
+ else
103
+ ::Zip::DOSTime.now
104
+ end
105
+ @extra =
106
+ extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
107
+
108
+ set_compression_level_flags
109
+ end
110
+
111
+ # Is this entry encrypted?
112
+ def encrypted?
113
+ gp_flags & 1 == 1
114
+ end
115
+
116
+ def incomplete? # :nodoc:
117
+ (gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
118
+ end
119
+
120
+ # The uncompressed size of the entry.
121
+ def size
122
+ @size || 0
123
+ end
124
+
125
+ # Get a timestamp component of this entry.
126
+ #
127
+ # Returns modification time by default.
128
+ def time(component: :mtime)
129
+ time =
130
+ if @extra[:universaltime]
131
+ @extra[:universaltime].send(component)
132
+ elsif @extra[:ntfs]
133
+ @extra[:ntfs].send(component)
134
+ end
70
135
 
71
- @ftype = name_is_directory? ? :directory : :file
72
- @extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.is_a?(::Zip::ExtraField)
136
+ # Standard time field in central directory has local time
137
+ # under archive creator. Then, we can't get timezone.
138
+ time || (@time if component == :mtime)
73
139
  end
74
140
 
75
- def time
76
- if @extra['UniversalTime']
77
- @extra['UniversalTime'].mtime
78
- elsif @extra['NTFS']
79
- @extra['NTFS'].mtime
80
- else
81
- # Standard time field in central directory has local time
82
- # under archive creator. Then, we can't get timezone.
83
- @time
84
- end
141
+ alias mtime time
142
+
143
+ # Get the last access time of this entry, if available.
144
+ def atime
145
+ time(component: :atime)
85
146
  end
86
147
 
87
- alias mtime time
148
+ # Get the creation time of this entry, if available.
149
+ def ctime
150
+ time(component: :ctime)
151
+ end
88
152
 
89
- def time=(value)
90
- unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
91
- @extra.create('UniversalTime')
153
+ # Set a timestamp component of this entry.
154
+ #
155
+ # Sets modification time by default.
156
+ def time=(value, component: :mtime)
157
+ @dirty = true
158
+ unless @extra.member?(:universaltime) || @extra.member?(:ntfs)
159
+ @extra.create(:universaltime)
92
160
  end
93
- (@extra['UniversalTime'] || @extra['NTFS']).mtime = value
94
- @time = value
161
+
162
+ value = DOSTime.from_time(value)
163
+ comp = "#{component}=" unless component.to_s.end_with?('=')
164
+ (@extra[:universaltime] || @extra[:ntfs]).send(comp, value)
165
+ @time = value if component == :mtime
166
+ end
167
+
168
+ alias mtime= time=
169
+
170
+ # Set the last access time of this entry.
171
+ def atime=(value)
172
+ send(:time=, value, component: :atime)
173
+ end
174
+
175
+ # Set the creation time of this entry.
176
+ def ctime=(value)
177
+ send(:time=, value, component: :ctime)
178
+ end
179
+
180
+ # Does this entry return time fields with accurate timezone information?
181
+ def absolute_time?
182
+ @extra.member?(:universaltime) || @extra.member?(:ntfs)
183
+ end
184
+
185
+ # Return the compression method for this entry.
186
+ #
187
+ # Returns STORED if the entry is a directory or if the compression
188
+ # level is 0.
189
+ def compression_method
190
+ return STORED if ftype == :directory || @compression_level == 0
191
+
192
+ @compression_method
95
193
  end
96
194
 
97
- def file_type_is?(type)
98
- raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
99
- @ftype == type
195
+ # Set the compression method for this entry.
196
+ def compression_method=(method)
197
+ @dirty = true
198
+ @compression_method = (ftype == :directory ? STORED : method)
199
+ end
200
+
201
+ # Does this entry use the ZIP64 extensions?
202
+ def zip64?
203
+ !@extra[:zip64].nil?
204
+ end
205
+
206
+ # Is this entry encrypted with AES encryption?
207
+ def aes?
208
+ !@extra[:aes].nil?
209
+ end
210
+
211
+ def file_type_is?(type) # :nodoc:
212
+ ftype == type
213
+ end
214
+
215
+ def ftype # :nodoc:
216
+ @ftype ||= name_is_directory? ? :directory : :file
100
217
  end
101
218
 
102
219
  # Dynamic checkers
103
220
  %w[directory file symlink].each do |k|
104
- define_method "#{k}?" do
221
+ define_method :"#{k}?" do
105
222
  file_type_is?(k.to_sym)
106
223
  end
107
224
  end
108
225
 
109
- def name_is_directory? #:nodoc:all
226
+ def name_is_directory? # :nodoc:
110
227
  @name.end_with?('/')
111
228
  end
112
229
 
113
230
  # Is the name a relative path, free of `..` patterns that could lead to
114
231
  # path traversal attacks? This does NOT handle symlinks; if the path
115
232
  # contains symlinks, this check is NOT enough to guarantee safety.
116
- def name_safe?
233
+ def name_safe? # :nodoc:
117
234
  cleanpath = Pathname.new(@name).cleanpath
118
235
  return false unless cleanpath.relative?
236
+
119
237
  root = ::File::SEPARATOR
120
- naive_expanded_path = ::File.join(root, cleanpath.to_s)
121
- ::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
238
+ naive = Regexp.escape(::File.join(root, cleanpath.to_s))
239
+ # Allow for Windows drive mappings at the root.
240
+ ::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
122
241
  end
123
242
 
124
- def local_entry_offset #:nodoc:all
243
+ def local_entry_offset # :nodoc:
125
244
  local_header_offset + @local_header_size
126
245
  end
127
246
 
128
- def name_size
247
+ def name_size # :nodoc:
129
248
  @name ? @name.bytesize : 0
130
249
  end
131
250
 
132
- def extra_size
251
+ def extra_size # :nodoc:
133
252
  @extra ? @extra.local_size : 0
134
253
  end
135
254
 
136
- def comment_size
255
+ def comment_size # :nodoc:
137
256
  @comment ? @comment.bytesize : 0
138
257
  end
139
258
 
140
- def calculate_local_header_size #:nodoc:all
259
+ def calculate_local_header_size # :nodoc:
141
260
  LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
142
261
  end
143
262
 
144
263
  # check before rewriting an entry (after file sizes are known)
145
264
  # that we didn't change the header size (and thus clobber file data or something)
146
- def verify_local_header_size!
265
+ def verify_local_header_size! # :nodoc:
147
266
  return if @local_header_size.nil?
267
+
148
268
  new_size = calculate_local_header_size
149
- raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
269
+ return unless @local_header_size != new_size
270
+
271
+ raise Error,
272
+ "Local header size changed (#{@local_header_size} -> #{new_size})"
150
273
  end
151
274
 
152
- def cdir_header_size #:nodoc:all
275
+ def cdir_header_size # :nodoc:
153
276
  CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
154
277
  (@extra ? @extra.c_dir_size : 0) + comment_size
155
278
  end
156
279
 
157
- def next_header_offset #:nodoc:all
158
- local_entry_offset + compressed_size + data_descriptor_size
280
+ def next_header_offset # :nodoc:
281
+ local_entry_offset + compressed_size
159
282
  end
160
283
 
161
- # Extracts entry to file dest_path (defaults to @name).
162
- # NB: The caller is responsible for making sure dest_path is safe, if it
163
- # is passed.
164
- def extract(dest_path = nil, &block)
165
- if dest_path.nil? && !name_safe?
166
- puts "WARNING: skipped #{@name} as unsafe"
284
+ # Extracts this entry to a file at `entry_path`, with
285
+ # `destination_directory` as the base location in the filesystem.
286
+ #
287
+ # NB: The caller is responsible for making sure `destination_directory` is
288
+ # safe, if it is passed.
289
+ def extract(entry_path = @name, destination_directory: '.', &block)
290
+ dest_dir = ::File.absolute_path(destination_directory || '.')
291
+ extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
292
+
293
+ unless extract_path.start_with?(dest_dir + ::File::SEPARATOR) || dest_dir == ::File::SEPARATOR
294
+ warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
167
295
  return self
168
296
  end
169
297
 
170
- dest_path ||= @name
171
298
  block ||= proc { ::Zip.on_exists_proc }
172
299
 
173
- if directory? || file? || symlink?
174
- __send__("create_#{@ftype}", dest_path, &block)
175
- else
176
- raise "unknown file type #{inspect}"
177
- end
300
+ raise "unknown file type #{inspect}" unless directory? || file? || symlink?
178
301
 
302
+ __send__(:"create_#{ftype}", extract_path, &block)
179
303
  self
180
304
  end
181
305
 
182
- def to_s
306
+ def to_s # :nodoc:
183
307
  @name
184
308
  end
185
309
 
186
310
  class << self
187
- def read_zip_short(io) # :nodoc:
188
- io.read(2).unpack('v')[0]
189
- end
190
-
191
- def read_zip_long(io) # :nodoc:
192
- io.read(4).unpack('V')[0]
193
- end
194
-
195
- def read_zip_64_long(io) # :nodoc:
196
- io.read(8).unpack('Q<')[0]
197
- end
198
-
199
- def read_c_dir_entry(io) #:nodoc:all
311
+ def read_c_dir_entry(io) # :nodoc:
200
312
  path = if io.respond_to?(:path)
201
313
  io.path
202
314
  else
@@ -209,18 +321,18 @@ module Zip
209
321
  nil
210
322
  end
211
323
 
212
- def read_local_entry(io)
324
+ def read_local_entry(io) # :nodoc:
213
325
  entry = new(io)
214
326
  entry.read_local_entry(io)
215
327
  entry
328
+ rescue SplitArchiveError
329
+ raise
216
330
  rescue Error
217
331
  nil
218
332
  end
219
333
  end
220
334
 
221
- public
222
-
223
- def unpack_local_entry(buf)
335
+ def unpack_local_entry(buf) # :nodoc:
224
336
  @header_signature,
225
337
  @version,
226
338
  @fstype,
@@ -235,61 +347,76 @@ module Zip
235
347
  @extra_length = buf.unpack('VCCvvvvVVVvv')
236
348
  end
237
349
 
238
- def read_local_entry(io) #:nodoc:all
239
- @local_header_offset = io.tell
350
+ def read_local_entry(io) # :nodoc:
351
+ @dirty = false # No changes at this point.
352
+ current_offset = io.tell
240
353
 
241
- static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
354
+ read_local_header_fields(io)
242
355
 
243
- unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
244
- raise Error, 'Premature end of file. Not enough data for zip entry local header'
245
- end
356
+ if @header_signature == SPLIT_FILE_SIGNATURE
357
+ raise SplitArchiveError if current_offset.zero?
246
358
 
247
- unpack_local_entry(static_sized_fields_buf)
359
+ # Rewind, skipping the data descriptor, then try to read the local header again.
360
+ current_offset += 16
361
+ io.seek(current_offset)
362
+ read_local_header_fields(io)
363
+ end
248
364
 
249
- unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
250
- raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
365
+ unless @header_signature == LOCAL_ENTRY_SIGNATURE
366
+ raise Error, "Zip local header magic not found at location '#{current_offset}'"
251
367
  end
368
+
369
+ @local_header_offset = current_offset
370
+
252
371
  set_time(@last_mod_date, @last_mod_time)
253
372
 
254
373
  @name = io.read(@name_length)
255
- extra = io.read(@extra_length)
256
-
257
- @name.tr!('\\', '/')
258
374
  if ::Zip.force_entry_names_encoding
259
375
  @name.force_encoding(::Zip.force_entry_names_encoding)
260
376
  end
377
+ @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
261
378
 
379
+ # We need to do this here because `initialize` has so many side-effects.
380
+ # :-(
381
+ @ftype = name_is_directory? ? :directory : :file
382
+
383
+ extra = io.read(@extra_length)
262
384
  if extra && extra.bytesize != @extra_length
263
385
  raise ::Zip::Error, 'Truncated local zip entry header'
264
- else
265
- if @extra.is_a?(::Zip::ExtraField)
266
- @extra.merge(extra) if extra
267
- else
268
- @extra = ::Zip::ExtraField.new(extra)
269
- end
270
386
  end
387
+
388
+ read_extra_field(extra, local: true)
271
389
  parse_zip64_extra(true)
390
+ parse_aes_extra
272
391
  @local_header_size = calculate_local_header_size
273
392
  end
274
393
 
275
- def pack_local_entry
276
- zip64 = @extra['Zip64']
394
+ def pack_local_entry # :nodoc:
395
+ zip64 = @extra[:zip64]
277
396
  [::Zip::LOCAL_ENTRY_SIGNATURE,
278
397
  @version_needed_to_extract, # version needed to extract
279
398
  @gp_flags, # @gp_flags
280
- @compression_method,
399
+ compression_method,
281
400
  @time.to_binary_dos_time, # @last_mod_time
282
401
  @time.to_binary_dos_date, # @last_mod_date
283
402
  @crc,
284
403
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
285
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
404
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
286
405
  name_size,
287
406
  @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
288
407
  end
289
408
 
290
- def write_local_entry(io, rewrite = false) #:nodoc:all
291
- prep_zip64_extra(true)
292
- verify_local_header_size! if rewrite
409
+ def write_local_entry(io, suppress_extra_fields: false, rewrite: false) # :nodoc:
410
+ prep_local_zip64_extra
411
+
412
+ # If we are rewriting the local header, then we verify that we haven't changed
413
+ # its size. At this point we have to keep extra fields if they are present.
414
+ if rewrite
415
+ verify_local_header_size!
416
+ elsif suppress_extra_fields
417
+ @extra.suppress_fields!(suppress_extra_fields)
418
+ end
419
+
293
420
  @local_header_offset = io.tell
294
421
 
295
422
  io << pack_local_entry
@@ -299,7 +426,7 @@ module Zip
299
426
  @local_header_size = io.tell - @local_header_offset
300
427
  end
301
428
 
302
- def unpack_c_dir_entry(buf)
429
+ def unpack_c_dir_entry(buf) # :nodoc:
303
430
  @header_signature,
304
431
  @version, # version of encoding software
305
432
  @fstype, # filesystem type
@@ -317,13 +444,10 @@ module Zip
317
444
  _, # diskNumberStart
318
445
  @internal_file_attributes,
319
446
  @external_file_attributes,
320
- @local_header_offset,
321
- @name,
322
- @extra,
323
- @comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
447
+ @local_header_offset = buf.unpack('VCCvvvvvVVVvvvvvVV')
324
448
  end
325
449
 
326
- def set_ftype_from_c_dir_entry
450
+ def set_ftype_from_c_dir_entry # :nodoc:
327
451
  @ftype = case @fstype
328
452
  when ::Zip::FSTYPE_UNIX
329
453
  @unix_perms = (@external_file_attributes >> 16) & 0o7777
@@ -335,8 +459,9 @@ module Zip
335
459
  when ::Zip::FILE_TYPE_SYMLINK
336
460
  :symlink
337
461
  else
338
- # best case guess for whether it is a file or not
339
- # Otherwise this would be set to unknown and that entry would never be able to extracted
462
+ # Best case guess for whether it is a file or not.
463
+ # Otherwise this would be set to unknown and that
464
+ # entry would never be able to be extracted.
340
465
  if name_is_directory?
341
466
  :directory
342
467
  else
@@ -352,44 +477,52 @@ module Zip
352
477
  end
353
478
  end
354
479
 
355
- def check_c_dir_entry_static_header_length(buf)
356
- return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
480
+ def check_c_dir_entry_static_header_length(buf) # :nodoc:
481
+ return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
482
+
357
483
  raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
358
484
  end
359
485
 
360
- def check_c_dir_entry_signature
361
- return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
486
+ def check_c_dir_entry_signature # :nodoc:
487
+ return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
488
+
362
489
  raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
363
490
  end
364
491
 
365
- def check_c_dir_entry_comment_size
492
+ def check_c_dir_entry_comment_size # :nodoc:
366
493
  return if @comment && @comment.bytesize == @comment_length
494
+
367
495
  raise ::Zip::Error, 'Truncated cdir zip entry header'
368
496
  end
369
497
 
370
- def read_c_dir_extra_field(io)
371
- if @extra.is_a?(::Zip::ExtraField)
372
- @extra.merge(io.read(@extra_length))
498
+ def read_extra_field(buf, local: false) # :nodoc:
499
+ if @extra.kind_of?(::Zip::ExtraField)
500
+ @extra.merge(buf, local: local) if buf
373
501
  else
374
- @extra = ::Zip::ExtraField.new(io.read(@extra_length))
502
+ @extra = ::Zip::ExtraField.new(buf, local: local)
375
503
  end
376
504
  end
377
505
 
378
- def read_c_dir_entry(io) #:nodoc:all
506
+ def read_c_dir_entry(io) # :nodoc:
507
+ @dirty = false # No changes at this point.
379
508
  static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
380
509
  check_c_dir_entry_static_header_length(static_sized_fields_buf)
381
510
  unpack_c_dir_entry(static_sized_fields_buf)
382
511
  check_c_dir_entry_signature
383
512
  set_time(@last_mod_date, @last_mod_time)
513
+
384
514
  @name = io.read(@name_length)
385
515
  if ::Zip.force_entry_names_encoding
386
516
  @name.force_encoding(::Zip.force_entry_names_encoding)
387
517
  end
388
- read_c_dir_extra_field(io)
518
+ @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
519
+
520
+ read_extra_field(io.read(@extra_length))
389
521
  @comment = io.read(@comment_length)
390
522
  check_c_dir_entry_comment_size
391
523
  set_ftype_from_c_dir_entry
392
524
  parse_zip64_extra(false)
525
+ parse_aes_extra
393
526
  end
394
527
 
395
528
  def file_stat(path) # :nodoc:
@@ -401,64 +534,72 @@ module Zip
401
534
  end
402
535
 
403
536
  def get_extra_attributes_from_path(path) # :nodoc:
404
- return if Zip::RUNNING_ON_WINDOWS
405
- stat = file_stat(path)
537
+ stat = file_stat(path)
538
+ @time = DOSTime.from_time(stat.mtime)
539
+ return if ::Zip::RUNNING_ON_WINDOWS
540
+
406
541
  @unix_uid = stat.uid
407
542
  @unix_gid = stat.gid
408
543
  @unix_perms = stat.mode & 0o7777
409
544
  end
410
545
 
411
- def set_unix_permissions_on_path(dest_path)
412
- # BUG: does not update timestamps into account
413
- # ignore setuid/setgid bits by default. honor if @restore_ownership
414
- unix_perms_mask = 0o1777
415
- unix_perms_mask = 0o7777 if @restore_ownership
416
- ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
417
- ::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
418
- # File::utimes()
546
+ # rubocop:disable Style/GuardClause
547
+ def set_unix_attributes_on_path(dest_path) # :nodoc:
548
+ # Ignore setuid/setgid bits by default. Honour if @restore_ownership.
549
+ unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
550
+ if @restore_permissions && @unix_perms
551
+ ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
552
+ end
553
+ if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
554
+ ::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
555
+ end
419
556
  end
557
+ # rubocop:enable Style/GuardClause
420
558
 
421
559
  def set_extra_attributes_on_path(dest_path) # :nodoc:
422
560
  return unless file? || directory?
423
561
 
424
562
  case @fstype
425
563
  when ::Zip::FSTYPE_UNIX
426
- set_unix_permissions_on_path(dest_path)
564
+ set_unix_attributes_on_path(dest_path)
427
565
  end
566
+
567
+ # Restore the timestamp on a file. This will either have come from the
568
+ # original source file that was copied into the archive, or from the
569
+ # creation date of the archive if there was no original source file.
570
+ ::FileUtils.touch(dest_path, mtime: time) if @restore_times
428
571
  end
429
572
 
430
- def pack_c_dir_entry
431
- zip64 = @extra['Zip64']
573
+ def pack_c_dir_entry # :nodoc:
574
+ zip64 = @extra[:zip64]
432
575
  [
433
576
  @header_signature,
434
577
  @version, # version of encoding software
435
578
  @fstype, # filesystem type
436
579
  @version_needed_to_extract, # @versionNeededToExtract
437
580
  @gp_flags, # @gp_flags
438
- @compression_method,
581
+ compression_method,
439
582
  @time.to_binary_dos_time, # @last_mod_time
440
583
  @time.to_binary_dos_date, # @last_mod_date
441
584
  @crc,
442
585
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
443
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
586
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
444
587
  name_size,
445
588
  @extra ? @extra.c_dir_size : 0,
446
589
  comment_size,
447
590
  zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
448
591
  @internal_file_attributes, # file type (binary=0, text=1)
449
592
  @external_file_attributes, # native filesystem attributes
450
- zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
451
- @name,
452
- @extra,
453
- @comment
593
+ zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset
454
594
  ].pack('VCCvvvvvVVVvvvvvVV')
455
595
  end
456
596
 
457
- def write_c_dir_entry(io) #:nodoc:all
458
- prep_zip64_extra(false)
597
+ def write_c_dir_entry(io, suppress_extra_fields: false) # :nodoc:
598
+ prep_cdir_zip64_extra
599
+
459
600
  case @fstype
460
601
  when ::Zip::FSTYPE_UNIX
461
- ft = case @ftype
602
+ ft = case ftype
462
603
  when :file
463
604
  @unix_perms ||= 0o644
464
605
  ::Zip::FILE_TYPE_FILE
@@ -471,10 +612,11 @@ module Zip
471
612
  end
472
613
 
473
614
  unless ft.nil?
474
- @external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
615
+ @external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 16
475
616
  end
476
617
  end
477
618
 
619
+ @extra.suppress_fields!(suppress_extra_fields) if suppress_extra_fields
478
620
  io << pack_c_dir_entry
479
621
 
480
622
  io << @name
@@ -482,42 +624,42 @@ module Zip
482
624
  io << @comment
483
625
  end
484
626
 
485
- def ==(other)
627
+ def ==(other) # :nodoc:
486
628
  return false unless other.class == self.class
629
+
487
630
  # Compares contents of local entry and exposed fields
488
- keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
631
+ %w[compression_method crc compressed_size size name extra filepath time].all? do |k|
489
632
  other.__send__(k.to_sym) == __send__(k.to_sym)
490
633
  end
491
- keys_equal && time.dos_equals(other.time)
492
634
  end
493
635
 
494
- def <=>(other)
636
+ def <=>(other) # :nodoc:
495
637
  to_s <=> other.to_s
496
638
  end
497
639
 
498
640
  # Returns an IO like object for the given ZipEntry.
499
641
  # Warning: may behave weird with symlinks.
500
642
  def get_input_stream(&block)
501
- if @ftype == :directory
502
- yield ::Zip::NullInputStream if block_given?
643
+ if ftype == :directory
644
+ yield ::Zip::NullInputStream if block
503
645
  ::Zip::NullInputStream
504
646
  elsif @filepath
505
- case @ftype
647
+ case ftype
506
648
  when :file
507
649
  ::File.open(@filepath, 'rb', &block)
508
650
  when :symlink
509
651
  linkpath = ::File.readlink(@filepath)
510
652
  stringio = ::StringIO.new(linkpath)
511
- yield(stringio) if block_given?
653
+ yield(stringio) if block
512
654
  stringio
513
655
  else
514
- raise "unknown @file_type #{@ftype}"
656
+ raise "unknown @file_type #{ftype}"
515
657
  end
516
658
  else
517
- zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
659
+ zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
518
660
  zis.instance_variable_set(:@complete_entry, self)
519
661
  zis.get_next_entry
520
- if block_given?
662
+ if block
521
663
  begin
522
664
  yield(zis)
523
665
  ensure
@@ -535,8 +677,8 @@ module Zip
535
677
  when 'file'
536
678
  if name_is_directory?
537
679
  raise ArgumentError,
538
- "entry name '#{newEntry}' indicates directory entry, but " \
539
- "'#{src_path}' is not a directory"
680
+ "entry name '#{@name}' indicates a directory entry, but " \
681
+ "'#{src_path}' is not a directory"
540
682
  end
541
683
  :file
542
684
  when 'directory'
@@ -545,8 +687,8 @@ module Zip
545
687
  when 'link'
546
688
  if name_is_directory?
547
689
  raise ArgumentError,
548
- "entry name '#{newEntry}' indicates directory entry, but " \
549
- "'#{src_path}' is not a directory"
690
+ "entry name '#{@name}' indicates a directory entry, but " \
691
+ "'#{src_path}' is not a directory"
550
692
  end
551
693
  :symlink
552
694
  else
@@ -554,27 +696,30 @@ module Zip
554
696
  end
555
697
 
556
698
  @filepath = src_path
699
+ @size = stat.size
557
700
  get_extra_attributes_from_path(@filepath)
558
701
  end
559
702
 
560
- def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
561
- if @ftype == :directory
562
- zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
703
+ def write_to_zip_output_stream(zip_output_stream) # :nodoc:
704
+ if ftype == :directory
705
+ zip_output_stream.put_next_entry(self)
563
706
  elsif @filepath
564
- zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
565
- get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) }
707
+ zip_output_stream.put_next_entry(self)
708
+ get_input_stream do |is|
709
+ ::Zip::IOExtras.copy_stream(zip_output_stream, is)
710
+ end
566
711
  else
567
712
  zip_output_stream.copy_raw_entry(self)
568
713
  end
569
714
  end
570
715
 
571
- def parent_as_string
716
+ def parent_as_string # :nodoc:
572
717
  entry_name = name.chomp('/')
573
718
  slash_index = entry_name.rindex('/')
574
719
  slash_index ? entry_name.slice(0, slash_index + 1) : nil
575
720
  end
576
721
 
577
- def get_raw_input_stream(&block)
722
+ def get_raw_input_stream(&block) # :nodoc:
578
723
  if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
579
724
  yield @zipfile
580
725
  else
@@ -582,58 +727,63 @@ module Zip
582
727
  end
583
728
  end
584
729
 
585
- def clean_up
586
- # By default, do nothing
730
+ def clean_up # :nodoc:
731
+ @dirty = false # Any changes are written at this point.
587
732
  end
588
733
 
589
734
  private
590
735
 
736
+ def read_local_header_fields(io) # :nodoc:
737
+ static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
738
+
739
+ unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
740
+ raise Error, 'Premature end of file. Not enough data for zip entry local header'
741
+ end
742
+
743
+ unpack_local_entry(static_sized_fields_buf)
744
+ end
745
+
591
746
  def set_time(binary_dos_date, binary_dos_time)
592
747
  @time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
593
748
  rescue ArgumentError
594
- warn 'Invalid date/time in zip entry' if ::Zip.warn_invalid_date
749
+ warn 'WARNING: invalid date/time in zip entry.' if ::Zip.warn_invalid_date
595
750
  end
596
751
 
597
752
  def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
598
753
  if ::File.exist?(dest_path) && !yield(self, dest_path)
599
- raise ::Zip::DestinationFileExistsError,
600
- "Destination '#{dest_path}' already exists"
754
+ raise DestinationExistsError, dest_path
601
755
  end
756
+
602
757
  ::File.open(dest_path, 'wb') do |os|
603
758
  get_input_stream do |is|
604
- set_extra_attributes_on_path(dest_path)
605
-
606
759
  bytes_written = 0
607
760
  warned = false
608
- buf = ''.dup
609
- while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
761
+ while (buf = is.sysread(Decompressor::CHUNK_SIZE))
610
762
  os << buf
611
763
  bytes_written += buf.bytesize
612
- if bytes_written > size && !warned
613
- message = "Entry #{name} should be #{size}B but is larger when inflated"
614
- if ::Zip.validate_entry_sizes
615
- raise ::Zip::EntrySizeError, message
616
- else
617
- puts "WARNING: #{message}"
618
- warned = true
619
- end
620
- end
764
+ next unless bytes_written > size && !warned
765
+
766
+ error = EntrySizeError.new(self)
767
+ raise error if Zip.validate_entry_sizes
768
+
769
+ warn "WARNING: #{error.message}"
770
+ warned = true
621
771
  end
622
772
  end
623
773
  end
774
+
775
+ set_extra_attributes_on_path(dest_path)
624
776
  end
625
777
 
626
778
  def create_directory(dest_path)
627
779
  return if ::File.directory?(dest_path)
780
+
628
781
  if ::File.exist?(dest_path)
629
- if block_given? && yield(self, dest_path)
630
- ::FileUtils.rm_f dest_path
631
- else
632
- raise ::Zip::DestinationFileExistsError,
633
- "Cannot create directory '#{dest_path}'. " \
634
- 'A file already exists with that name'
635
- end
782
+ raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
783
+
784
+ ::FileUtils.rm_f dest_path
636
785
  end
786
+
637
787
  ::FileUtils.mkdir_p(dest_path)
638
788
  set_extra_attributes_on_path(dest_path)
639
789
  end
@@ -642,56 +792,90 @@ module Zip
642
792
  def create_symlink(dest_path)
643
793
  # TODO: Symlinks pose security challenges. Symlink support temporarily
644
794
  # removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
645
- puts "WARNING: skipped symlink #{dest_path}"
795
+ warn "WARNING: skipped symlink '#{dest_path}'."
646
796
  end
647
797
 
648
798
  # apply missing data from the zip64 extra information field, if present
649
799
  # (required when file sizes exceed 2**32, but can be used for all files)
650
- def parse_zip64_extra(for_local_header) #:nodoc:all
651
- return if @extra['Zip64'].nil?
800
+ def parse_zip64_extra(for_local_header) # :nodoc:
801
+ return unless zip64?
802
+
652
803
  if for_local_header
653
- @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
804
+ @size, @compressed_size = @extra[:zip64].parse(@size, @compressed_size)
654
805
  else
655
- @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(@size, @compressed_size, @local_header_offset)
806
+ @size, @compressed_size, @local_header_offset = @extra[:zip64].parse(
807
+ @size, @compressed_size, @local_header_offset
808
+ )
809
+ end
810
+ end
811
+
812
+ def parse_aes_extra # :nodoc:
813
+ return unless aes?
814
+
815
+ if @extra[:aes].vendor_id != 'AE'
816
+ raise Error, "Unsupported encryption method #{@extra[:aes].vendor_id}"
817
+ end
818
+
819
+ unless ::Zip::AESEncryption::VERSIONS.include? @extra[:aes].vendor_version
820
+ raise Error, "Unsupported encryption style #{@extra[:aes].vendor_version}"
821
+ end
822
+
823
+ @compression_method = @extra[:aes].compression_method if ftype != :directory
824
+ end
825
+
826
+ # For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
827
+ # indicate compression level. This seems to be mainly cosmetic but they are
828
+ # generally set by other tools - including in docx files. It is these flags
829
+ # that are used by commandline tools (and elsewhere) to give an indication
830
+ # of how compressed a file is. See the PKWARE APPNOTE for more information:
831
+ # https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
832
+ #
833
+ # It's safe to simply OR these flags here as compression_level is read only.
834
+ def set_compression_level_flags
835
+ return unless compression_method == DEFLATED
836
+
837
+ case @compression_level
838
+ when 1
839
+ @gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
840
+ when 2
841
+ @gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
842
+ when 8, 9
843
+ @gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
656
844
  end
657
845
  end
658
846
 
659
- def data_descriptor_size
660
- (@gp_flags & 0x0008) > 0 ? 16 : 0
847
+ # rubocop:disable Style/GuardClause
848
+ def prep_local_zip64_extra
849
+ return unless ::Zip.write_zip64_support
850
+ return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
851
+
852
+ # Might not know size here, so need ZIP64 just in case.
853
+ # If we already have a ZIP64 extra (placeholder) then we must fill it in.
854
+ if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
855
+ @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
856
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
857
+
858
+ # Local header always includes size and compressed size.
859
+ zip64.original_size = @size || 0
860
+ zip64.compressed_size = @compressed_size
861
+ end
661
862
  end
662
863
 
663
- # create a zip64 extra information field if we need one
664
- def prep_zip64_extra(for_local_header) #:nodoc:all
864
+ def prep_cdir_zip64_extra
665
865
  return unless ::Zip.write_zip64_support
666
- need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
667
- need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
668
- if need_zip64
866
+
867
+ if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
868
+ @local_header_offset >= 0xFFFFFFFF
669
869
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
670
- @extra.delete('Zip64Placeholder')
671
- zip64 = @extra.create('Zip64')
672
- if for_local_header
673
- # local header always includes size and compressed size
674
- zip64.original_size = @size
675
- zip64.compressed_size = @compressed_size
676
- else
677
- # central directory entry entries include whichever fields are necessary
678
- zip64.original_size = @size if @size >= 0xFFFFFFFF
679
- zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
680
- zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
681
- end
682
- else
683
- @extra.delete('Zip64')
870
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
684
871
 
685
- # if this is a local header entry, create a placeholder
686
- # so we have room to write a zip64 extra field afterward
687
- # (we won't know if it's needed until the file data is written)
688
- if for_local_header
689
- @extra.create('Zip64Placeholder')
690
- else
691
- @extra.delete('Zip64Placeholder')
692
- end
872
+ # Central directory entry entries include whichever fields are necessary.
873
+ zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
874
+ zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
875
+ zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
693
876
  end
694
877
  end
878
+ # rubocop:enable Style/GuardClause
695
879
  end
696
880
  end
697
881