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