rubyzip 2.1.0 → 3.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +538 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +235 -48
  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 +123 -0
  11. data/lib/zip/crypto/decrypted_io.rb +52 -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 +79 -14
  19. data/lib/zip/entry.rb +430 -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 +17 -17
  24. data/lib/zip/extra_field/ntfs.rb +10 -4
  25. data/lib/zip/extra_field/old_unix.rb +8 -4
  26. data/lib/zip/extra_field/universal_time.rb +6 -1
  27. data/lib/zip/extra_field/unix.rb +9 -5
  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 +211 -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 +12 -9
  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 +76 -51
  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 +9 -5
  52. data/lib/zip/version.rb +4 -1
  53. data/lib/zip.rb +31 -5
  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 +84 -28
  63. data/TODO +0 -15
  64. 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,169 +58,257 @@ 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
- 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
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)
95
199
  end
96
200
 
97
- def file_type_is?(type)
98
- raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
99
- @ftype == type
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
- warn "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.
378
+
379
+ # We need to do this here because `initialize` has so many side-effects.
380
+ # :-(
381
+ @ftype = name_is_directory? ? :directory : :file
261
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,26 +534,27 @@ 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
- @time = ::Zip::DOSTime.from_time(stat.mtime)
410
544
  end
411
545
 
412
- def set_unix_attributes_on_path(dest_path)
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
-
419
- # Restore the timestamp on a file. This will either have come from the
420
- # original source file that was copied into the archive, or from the
421
- # creation date of the archive if there was no original source file.
422
- ::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
423
556
  end
557
+ # rubocop:enable Style/GuardClause
424
558
 
425
559
  def set_extra_attributes_on_path(dest_path) # :nodoc:
426
560
  return unless file? || directory?
@@ -429,40 +563,43 @@ module Zip
429
563
  when ::Zip::FSTYPE_UNIX
430
564
  set_unix_attributes_on_path(dest_path)
431
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
432
571
  end
433
572
 
434
- def pack_c_dir_entry
435
- zip64 = @extra['Zip64']
573
+ def pack_c_dir_entry # :nodoc:
574
+ zip64 = @extra[:zip64]
436
575
  [
437
576
  @header_signature,
438
577
  @version, # version of encoding software
439
578
  @fstype, # filesystem type
440
579
  @version_needed_to_extract, # @versionNeededToExtract
441
580
  @gp_flags, # @gp_flags
442
- @compression_method,
581
+ compression_method,
443
582
  @time.to_binary_dos_time, # @last_mod_time
444
583
  @time.to_binary_dos_date, # @last_mod_date
445
584
  @crc,
446
585
  zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
447
- zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
586
+ zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
448
587
  name_size,
449
588
  @extra ? @extra.c_dir_size : 0,
450
589
  comment_size,
451
590
  zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
452
591
  @internal_file_attributes, # file type (binary=0, text=1)
453
592
  @external_file_attributes, # native filesystem attributes
454
- zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
455
- @name,
456
- @extra,
457
- @comment
593
+ zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset
458
594
  ].pack('VCCvvvvvVVVvvvvvVV')
459
595
  end
460
596
 
461
- def write_c_dir_entry(io) #:nodoc:all
462
- prep_zip64_extra(false)
597
+ def write_c_dir_entry(io, suppress_extra_fields: false) # :nodoc:
598
+ prep_cdir_zip64_extra
599
+
463
600
  case @fstype
464
601
  when ::Zip::FSTYPE_UNIX
465
- ft = case @ftype
602
+ ft = case ftype
466
603
  when :file
467
604
  @unix_perms ||= 0o644
468
605
  ::Zip::FILE_TYPE_FILE
@@ -475,10 +612,11 @@ module Zip
475
612
  end
476
613
 
477
614
  unless ft.nil?
478
- @external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
615
+ @external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 16
479
616
  end
480
617
  end
481
618
 
619
+ @extra.suppress_fields!(suppress_extra_fields) if suppress_extra_fields
482
620
  io << pack_c_dir_entry
483
621
 
484
622
  io << @name
@@ -486,42 +624,43 @@ module Zip
486
624
  io << @comment
487
625
  end
488
626
 
489
- def ==(other)
627
+ def ==(other) # :nodoc:
490
628
  return false unless other.class == self.class
629
+
491
630
  # Compares contents of local entry and exposed fields
492
- 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|
493
632
  other.__send__(k.to_sym) == __send__(k.to_sym)
494
633
  end
495
- keys_equal && time.dos_equals(other.time)
496
634
  end
497
635
 
498
- def <=>(other)
636
+ def <=>(other) # :nodoc:
499
637
  to_s <=> other.to_s
500
638
  end
501
639
 
502
640
  # Returns an IO like object for the given ZipEntry.
641
+ # Optional decrypter can be provided for an encrypted entry
503
642
  # Warning: may behave weird with symlinks.
504
- def get_input_stream(&block)
505
- if @ftype == :directory
506
- yield ::Zip::NullInputStream if block_given?
643
+ def get_input_stream(decrypter: nil, &block)
644
+ if ftype == :directory
645
+ yield ::Zip::NullInputStream if block
507
646
  ::Zip::NullInputStream
508
647
  elsif @filepath
509
- case @ftype
648
+ case ftype
510
649
  when :file
511
650
  ::File.open(@filepath, 'rb', &block)
512
651
  when :symlink
513
652
  linkpath = ::File.readlink(@filepath)
514
653
  stringio = ::StringIO.new(linkpath)
515
- yield(stringio) if block_given?
654
+ yield(stringio) if block
516
655
  stringio
517
656
  else
518
- raise "unknown @file_type #{@ftype}"
657
+ raise "unknown @file_type #{ftype}"
519
658
  end
520
659
  else
521
- zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
660
+ zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset, decrypter: decrypter)
522
661
  zis.instance_variable_set(:@complete_entry, self)
523
662
  zis.get_next_entry
524
- if block_given?
663
+ if block
525
664
  begin
526
665
  yield(zis)
527
666
  ensure
@@ -539,8 +678,8 @@ module Zip
539
678
  when 'file'
540
679
  if name_is_directory?
541
680
  raise ArgumentError,
542
- "entry name '#{newEntry}' indicates directory entry, but " \
543
- "'#{src_path}' is not a directory"
681
+ "entry name '#{@name}' indicates a directory entry, but " \
682
+ "'#{src_path}' is not a directory"
544
683
  end
545
684
  :file
546
685
  when 'directory'
@@ -549,8 +688,8 @@ module Zip
549
688
  when 'link'
550
689
  if name_is_directory?
551
690
  raise ArgumentError,
552
- "entry name '#{newEntry}' indicates directory entry, but " \
553
- "'#{src_path}' is not a directory"
691
+ "entry name '#{@name}' indicates a directory entry, but " \
692
+ "'#{src_path}' is not a directory"
554
693
  end
555
694
  :symlink
556
695
  else
@@ -558,27 +697,30 @@ module Zip
558
697
  end
559
698
 
560
699
  @filepath = src_path
700
+ @size = stat.size
561
701
  get_extra_attributes_from_path(@filepath)
562
702
  end
563
703
 
564
- def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
565
- if @ftype == :directory
566
- zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
704
+ def write_to_zip_output_stream(zip_output_stream) # :nodoc:
705
+ if ftype == :directory
706
+ zip_output_stream.put_next_entry(self)
567
707
  elsif @filepath
568
- zip_output_stream.put_next_entry(self, nil, nil, compression_method || ::Zip::Entry::DEFLATED)
569
- get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) }
708
+ zip_output_stream.put_next_entry(self)
709
+ get_input_stream do |is|
710
+ ::Zip::IOExtras.copy_stream(zip_output_stream, is)
711
+ end
570
712
  else
571
713
  zip_output_stream.copy_raw_entry(self)
572
714
  end
573
715
  end
574
716
 
575
- def parent_as_string
717
+ def parent_as_string # :nodoc:
576
718
  entry_name = name.chomp('/')
577
719
  slash_index = entry_name.rindex('/')
578
720
  slash_index ? entry_name.slice(0, slash_index + 1) : nil
579
721
  end
580
722
 
581
- def get_raw_input_stream(&block)
723
+ def get_raw_input_stream(&block) # :nodoc:
582
724
  if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
583
725
  yield @zipfile
584
726
  else
@@ -586,12 +728,22 @@ module Zip
586
728
  end
587
729
  end
588
730
 
589
- def clean_up
590
- # By default, do nothing
731
+ def clean_up # :nodoc:
732
+ @dirty = false # Any changes are written at this point.
591
733
  end
592
734
 
593
735
  private
594
736
 
737
+ def read_local_header_fields(io) # :nodoc:
738
+ static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
739
+
740
+ unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
741
+ raise Error, 'Premature end of file. Not enough data for zip entry local header'
742
+ end
743
+
744
+ unpack_local_entry(static_sized_fields_buf)
745
+ end
746
+
595
747
  def set_time(binary_dos_date, binary_dos_time)
596
748
  @time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
597
749
  rescue ArgumentError
@@ -600,26 +752,23 @@ module Zip
600
752
 
601
753
  def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
602
754
  if ::File.exist?(dest_path) && !yield(self, dest_path)
603
- raise ::Zip::DestinationFileExistsError,
604
- "Destination '#{dest_path}' already exists"
755
+ raise DestinationExistsError, dest_path
605
756
  end
757
+
606
758
  ::File.open(dest_path, 'wb') do |os|
607
759
  get_input_stream do |is|
608
760
  bytes_written = 0
609
761
  warned = false
610
- buf = ''.dup
611
- while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
762
+ while (buf = is.sysread(Decompressor::CHUNK_SIZE))
612
763
  os << buf
613
764
  bytes_written += buf.bytesize
614
- if bytes_written > size && !warned
615
- message = "entry '#{name}' should be #{size}B, but is larger when inflated."
616
- if ::Zip.validate_entry_sizes
617
- raise ::Zip::EntrySizeError, message
618
- else
619
- warn "WARNING: #{message}"
620
- warned = true
621
- end
622
- end
765
+ next unless bytes_written > size && !warned
766
+
767
+ error = EntrySizeError.new(self)
768
+ raise error if Zip.validate_entry_sizes
769
+
770
+ warn "WARNING: #{error.message}"
771
+ warned = true
623
772
  end
624
773
  end
625
774
  end
@@ -629,15 +778,13 @@ module Zip
629
778
 
630
779
  def create_directory(dest_path)
631
780
  return if ::File.directory?(dest_path)
781
+
632
782
  if ::File.exist?(dest_path)
633
- if block_given? && yield(self, dest_path)
634
- ::FileUtils.rm_f dest_path
635
- else
636
- raise ::Zip::DestinationFileExistsError,
637
- "Cannot create directory '#{dest_path}'. " \
638
- 'A file already exists with that name'
639
- end
783
+ raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
784
+
785
+ ::FileUtils.rm_f dest_path
640
786
  end
787
+
641
788
  ::FileUtils.mkdir_p(dest_path)
642
789
  set_extra_attributes_on_path(dest_path)
643
790
  end
@@ -651,51 +798,85 @@ module Zip
651
798
 
652
799
  # apply missing data from the zip64 extra information field, if present
653
800
  # (required when file sizes exceed 2**32, but can be used for all files)
654
- def parse_zip64_extra(for_local_header) #:nodoc:all
655
- return if @extra['Zip64'].nil?
801
+ def parse_zip64_extra(for_local_header) # :nodoc:
802
+ return unless zip64?
803
+
656
804
  if for_local_header
657
- @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
805
+ @size, @compressed_size = @extra[:zip64].parse(@size, @compressed_size)
658
806
  else
659
- @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
+ )
660
810
  end
661
811
  end
662
812
 
663
- def data_descriptor_size
664
- (@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
665
846
  end
666
847
 
667
- # create a zip64 extra information field if we need one
668
- def prep_zip64_extra(for_local_header) #:nodoc:all
848
+ # rubocop:disable Style/GuardClause
849
+ def prep_local_zip64_extra
669
850
  return unless ::Zip.write_zip64_support
670
- need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
671
- need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
672
- if need_zip64
851
+ return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
852
+
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
673
856
  @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
674
- @extra.delete('Zip64Placeholder')
675
- zip64 = @extra.create('Zip64')
676
- if for_local_header
677
- # local header always includes size and compressed size
678
- zip64.original_size = @size
679
- zip64.compressed_size = @compressed_size
680
- else
681
- # central directory entry entries include whichever fields are necessary
682
- zip64.original_size = @size if @size >= 0xFFFFFFFF
683
- zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
684
- zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
685
- end
686
- else
687
- @extra.delete('Zip64')
857
+ zip64 = @extra[:zip64] || @extra.create(:zip64)
688
858
 
689
- # if this is a local header entry, create a placeholder
690
- # so we have room to write a zip64 extra field afterward
691
- # (we won't know if it's needed until the file data is written)
692
- if for_local_header
693
- @extra.create('Zip64Placeholder')
694
- else
695
- @extra.delete('Zip64Placeholder')
696
- 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
697
877
  end
698
878
  end
879
+ # rubocop:enable Style/GuardClause
699
880
  end
700
881
  end
701
882