rubyzip 2.4.1 → 3.2.1

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