rubyzip 1.3.0 → 3.0.0.alpha

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