rubyzip 0.9.9 → 1.3.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 (135) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +284 -41
  3. data/Rakefile +11 -6
  4. data/TODO +0 -1
  5. data/lib/zip/central_directory.rb +208 -0
  6. data/lib/zip/compressor.rb +1 -2
  7. data/lib/zip/constants.rb +59 -7
  8. data/lib/zip/crypto/encryption.rb +11 -0
  9. data/lib/zip/crypto/null_encryption.rb +43 -0
  10. data/lib/zip/crypto/traditional_encryption.rb +99 -0
  11. data/lib/zip/decompressor.rb +4 -4
  12. data/lib/zip/deflater.rb +17 -13
  13. data/lib/zip/dos_time.rb +13 -14
  14. data/lib/zip/entry.rb +700 -0
  15. data/lib/zip/entry_set.rb +86 -0
  16. data/lib/zip/errors.rb +18 -0
  17. data/lib/zip/extra_field/generic.rb +43 -0
  18. data/lib/zip/extra_field/ntfs.rb +90 -0
  19. data/lib/zip/extra_field/old_unix.rb +44 -0
  20. data/lib/zip/extra_field/universal_time.rb +47 -0
  21. data/lib/zip/extra_field/unix.rb +37 -0
  22. data/lib/zip/extra_field/zip64.rb +68 -0
  23. data/lib/zip/extra_field/zip64_placeholder.rb +15 -0
  24. data/lib/zip/extra_field.rb +101 -0
  25. data/lib/zip/file.rb +443 -0
  26. data/lib/zip/{zipfilesystem.rb → filesystem.rb} +162 -157
  27. data/lib/zip/inflater.rb +29 -28
  28. data/lib/zip/input_stream.rb +173 -0
  29. data/lib/zip/ioextras/abstract_input_stream.rb +111 -0
  30. data/lib/zip/ioextras/abstract_output_stream.rb +43 -0
  31. data/lib/zip/ioextras.rb +21 -149
  32. data/lib/zip/null_compressor.rb +2 -2
  33. data/lib/zip/null_decompressor.rb +8 -6
  34. data/lib/zip/null_input_stream.rb +3 -2
  35. data/lib/zip/output_stream.rb +189 -0
  36. data/lib/zip/pass_thru_compressor.rb +6 -6
  37. data/lib/zip/pass_thru_decompressor.rb +19 -19
  38. data/lib/zip/{zip_streamable_directory.rb → streamable_directory.rb} +3 -3
  39. data/lib/zip/streamable_stream.rb +56 -0
  40. data/lib/zip/version.rb +3 -0
  41. data/lib/zip.rb +71 -0
  42. data/samples/example.rb +44 -32
  43. data/samples/example_filesystem.rb +16 -18
  44. data/samples/example_recursive.rb +33 -28
  45. data/samples/{gtkRubyzip.rb → gtk_ruby_zip.rb} +26 -28
  46. data/samples/qtzip.rb +22 -31
  47. data/samples/write_simple.rb +12 -13
  48. data/samples/zipfind.rb +31 -39
  49. data/test/basic_zip_file_test.rb +60 -0
  50. data/test/case_sensitivity_test.rb +69 -0
  51. data/test/central_directory_entry_test.rb +69 -0
  52. data/test/central_directory_test.rb +100 -0
  53. data/test/crypto/null_encryption_test.rb +57 -0
  54. data/test/crypto/traditional_encryption_test.rb +80 -0
  55. data/test/data/WarnInvalidDate.zip +0 -0
  56. data/test/data/file1.txt +46 -0
  57. data/test/data/file1.txt.deflatedData +0 -0
  58. data/test/data/file2.txt +1504 -0
  59. data/test/data/globTest/foo/bar/baz/foo.txt +0 -0
  60. data/test/data/globTest/foo.txt +0 -0
  61. data/test/data/globTest/food.txt +0 -0
  62. data/test/data/globTest.zip +0 -0
  63. data/test/data/gpbit3stored.zip +0 -0
  64. data/test/data/mimetype +1 -0
  65. data/test/data/notzippedruby.rb +7 -0
  66. data/test/data/ntfs.zip +0 -0
  67. data/test/data/oddExtraField.zip +0 -0
  68. data/test/data/path_traversal/Makefile +10 -0
  69. data/test/data/path_traversal/jwilk/README.md +5 -0
  70. data/test/data/path_traversal/jwilk/absolute1.zip +0 -0
  71. data/test/data/path_traversal/jwilk/absolute2.zip +0 -0
  72. data/test/data/path_traversal/jwilk/dirsymlink.zip +0 -0
  73. data/test/data/path_traversal/jwilk/dirsymlink2a.zip +0 -0
  74. data/test/data/path_traversal/jwilk/dirsymlink2b.zip +0 -0
  75. data/test/data/path_traversal/jwilk/relative0.zip +0 -0
  76. data/test/data/path_traversal/jwilk/relative2.zip +0 -0
  77. data/test/data/path_traversal/jwilk/symlink.zip +0 -0
  78. data/test/data/path_traversal/relative1.zip +0 -0
  79. data/test/data/path_traversal/tilde.zip +0 -0
  80. data/test/data/path_traversal/tuzovakaoff/README.md +3 -0
  81. data/test/data/path_traversal/tuzovakaoff/absolutepath.zip +0 -0
  82. data/test/data/path_traversal/tuzovakaoff/symlink.zip +0 -0
  83. data/test/data/rubycode.zip +0 -0
  84. data/test/data/rubycode2.zip +0 -0
  85. data/test/data/test.xls +0 -0
  86. data/test/data/testDirectory.bin +0 -0
  87. data/test/data/zip64-sample.zip +0 -0
  88. data/test/data/zipWithDirs.zip +0 -0
  89. data/test/data/zipWithEncryption.zip +0 -0
  90. data/test/deflater_test.rb +65 -0
  91. data/test/encryption_test.rb +42 -0
  92. data/test/entry_set_test.rb +163 -0
  93. data/test/entry_test.rb +154 -0
  94. data/test/errors_test.rb +35 -0
  95. data/test/extra_field_test.rb +76 -0
  96. data/test/file_extract_directory_test.rb +54 -0
  97. data/test/file_extract_test.rb +145 -0
  98. data/test/file_permissions_test.rb +65 -0
  99. data/test/file_split_test.rb +57 -0
  100. data/test/file_test.rb +666 -0
  101. data/test/filesystem/dir_iterator_test.rb +58 -0
  102. data/test/filesystem/directory_test.rb +139 -0
  103. data/test/filesystem/file_mutating_test.rb +87 -0
  104. data/test/filesystem/file_nonmutating_test.rb +508 -0
  105. data/test/filesystem/file_stat_test.rb +64 -0
  106. data/test/gentestfiles.rb +126 -0
  107. data/test/inflater_test.rb +14 -0
  108. data/test/input_stream_test.rb +182 -0
  109. data/test/ioextras/abstract_input_stream_test.rb +102 -0
  110. data/test/ioextras/abstract_output_stream_test.rb +106 -0
  111. data/test/ioextras/fake_io_test.rb +18 -0
  112. data/test/local_entry_test.rb +154 -0
  113. data/test/output_stream_test.rb +128 -0
  114. data/test/pass_thru_compressor_test.rb +30 -0
  115. data/test/pass_thru_decompressor_test.rb +14 -0
  116. data/test/path_traversal_test.rb +141 -0
  117. data/test/samples/example_recursive_test.rb +37 -0
  118. data/test/settings_test.rb +95 -0
  119. data/test/test_helper.rb +234 -0
  120. data/test/unicode_file_names_and_comments_test.rb +62 -0
  121. data/test/zip64_full_test.rb +51 -0
  122. data/test/zip64_support_test.rb +14 -0
  123. metadata +274 -41
  124. data/NEWS +0 -172
  125. data/lib/zip/settings.rb +0 -10
  126. data/lib/zip/tempfile_bugfixed.rb +0 -195
  127. data/lib/zip/zip.rb +0 -56
  128. data/lib/zip/zip_central_directory.rb +0 -135
  129. data/lib/zip/zip_entry.rb +0 -638
  130. data/lib/zip/zip_entry_set.rb +0 -77
  131. data/lib/zip/zip_extra_field.rb +0 -213
  132. data/lib/zip/zip_file.rb +0 -340
  133. data/lib/zip/zip_input_stream.rb +0 -144
  134. data/lib/zip/zip_output_stream.rb +0 -173
  135. data/lib/zip/zip_streamable_stream.rb +0 -47
data/lib/zip/entry.rb ADDED
@@ -0,0 +1,700 @@
1
+ require 'pathname'
2
+ module Zip
3
+ class Entry
4
+ STORED = 0
5
+ DEFLATED = 8
6
+ # 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
19
+ @local_header_offset = 0
20
+ @local_header_size = nil # not known until local entry is created or read
21
+ @internal_file_attributes = 1
22
+ @external_file_attributes = 0
23
+ @header_signature = ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
24
+
25
+ @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT
26
+ @version = VERSION_MADE_BY
27
+
28
+ @ftype = nil # unspecified or unknown
29
+ @filepath = nil
30
+ @gp_flags = 0
31
+ if ::Zip.unicode_names
32
+ @gp_flags |= EFS
33
+ @version = 63
34
+ end
35
+ @follow_symlinks = false
36
+
37
+ @restore_times = true
38
+ @restore_permissions = false
39
+ @restore_ownership = false
40
+ # BUG: need an extra field to support uid/gid's
41
+ @unix_uid = nil
42
+ @unix_gid = nil
43
+ @unix_perms = nil
44
+ # @posix_acl = nil
45
+ # @ntfs_acl = nil
46
+ @dirty = false
47
+ end
48
+
49
+ def check_name(name)
50
+ return unless name.start_with?('/')
51
+ raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
52
+ end
53
+
54
+ def initialize(*args)
55
+ name = args[1] || ''
56
+ check_name(name)
57
+
58
+ set_default_vars_values
59
+ @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
60
+
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
70
+
71
+ @ftype = name_is_directory? ? :directory : :file
72
+ @extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.is_a?(::Zip::ExtraField)
73
+ end
74
+
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
85
+ end
86
+
87
+ alias mtime time
88
+
89
+ def time=(value)
90
+ unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
91
+ @extra.create('UniversalTime')
92
+ end
93
+ (@extra['UniversalTime'] || @extra['NTFS']).mtime = value
94
+ @time = value
95
+ end
96
+
97
+ def file_type_is?(type)
98
+ raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
99
+ @ftype == type
100
+ end
101
+
102
+ # Dynamic checkers
103
+ %w[directory file symlink].each do |k|
104
+ define_method "#{k}?" do
105
+ file_type_is?(k.to_sym)
106
+ end
107
+ end
108
+
109
+ def name_is_directory? #:nodoc:all
110
+ @name.end_with?('/')
111
+ end
112
+
113
+ # Is the name a relative path, free of `..` patterns that could lead to
114
+ # path traversal attacks? This does NOT handle symlinks; if the path
115
+ # contains symlinks, this check is NOT enough to guarantee safety.
116
+ def name_safe?
117
+ cleanpath = Pathname.new(@name).cleanpath
118
+ return false unless cleanpath.relative?
119
+ root = ::File::SEPARATOR
120
+ naive_expanded_path = ::File.join(root, cleanpath.to_s)
121
+ ::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
122
+ end
123
+
124
+ def local_entry_offset #:nodoc:all
125
+ local_header_offset + @local_header_size
126
+ end
127
+
128
+ def name_size
129
+ @name ? @name.bytesize : 0
130
+ end
131
+
132
+ def extra_size
133
+ @extra ? @extra.local_size : 0
134
+ end
135
+
136
+ def comment_size
137
+ @comment ? @comment.bytesize : 0
138
+ end
139
+
140
+ def calculate_local_header_size #:nodoc:all
141
+ LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
142
+ end
143
+
144
+ # check before rewriting an entry (after file sizes are known)
145
+ # that we didn't change the header size (and thus clobber file data or something)
146
+ def verify_local_header_size!
147
+ return if @local_header_size.nil?
148
+ new_size = calculate_local_header_size
149
+ raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
150
+ end
151
+
152
+ def cdir_header_size #:nodoc:all
153
+ CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
154
+ (@extra ? @extra.c_dir_size : 0) + comment_size
155
+ end
156
+
157
+ def next_header_offset #:nodoc:all
158
+ local_entry_offset + compressed_size + data_descriptor_size
159
+ end
160
+
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"
167
+ return self
168
+ end
169
+
170
+ dest_path ||= @name
171
+ block ||= proc { ::Zip.on_exists_proc }
172
+
173
+ if directory? || file? || symlink?
174
+ __send__("create_#{@ftype}", dest_path, &block)
175
+ else
176
+ raise "unknown file type #{inspect}"
177
+ end
178
+
179
+ self
180
+ end
181
+
182
+ def to_s
183
+ @name
184
+ end
185
+
186
+ 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
200
+ path = if io.respond_to?(:path)
201
+ io.path
202
+ else
203
+ io
204
+ end
205
+ entry = new(path)
206
+ entry.read_c_dir_entry(io)
207
+ entry
208
+ rescue Error
209
+ nil
210
+ end
211
+
212
+ def read_local_entry(io)
213
+ entry = new(io)
214
+ entry.read_local_entry(io)
215
+ entry
216
+ rescue Error
217
+ nil
218
+ end
219
+ end
220
+
221
+ public
222
+
223
+ def unpack_local_entry(buf)
224
+ @header_signature,
225
+ @version,
226
+ @fstype,
227
+ @gp_flags,
228
+ @compression_method,
229
+ @last_mod_time,
230
+ @last_mod_date,
231
+ @crc,
232
+ @compressed_size,
233
+ @size,
234
+ @name_length,
235
+ @extra_length = buf.unpack('VCCvvvvVVVvv')
236
+ end
237
+
238
+ def read_local_entry(io) #:nodoc:all
239
+ @local_header_offset = io.tell
240
+
241
+ static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
242
+
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
246
+
247
+ unpack_local_entry(static_sized_fields_buf)
248
+
249
+ unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
250
+ raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
251
+ end
252
+ set_time(@last_mod_date, @last_mod_time)
253
+
254
+ @name = io.read(@name_length)
255
+ extra = io.read(@extra_length)
256
+
257
+ @name.tr!('\\', '/')
258
+ if ::Zip.force_entry_names_encoding
259
+ @name.force_encoding(::Zip.force_entry_names_encoding)
260
+ end
261
+
262
+ if extra && extra.bytesize != @extra_length
263
+ 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
+ end
271
+ parse_zip64_extra(true)
272
+ @local_header_size = calculate_local_header_size
273
+ end
274
+
275
+ def pack_local_entry
276
+ zip64 = @extra['Zip64']
277
+ [::Zip::LOCAL_ENTRY_SIGNATURE,
278
+ @version_needed_to_extract, # version needed to extract
279
+ @gp_flags, # @gp_flags
280
+ @compression_method,
281
+ @time.to_binary_dos_time, # @last_mod_time
282
+ @time.to_binary_dos_date, # @last_mod_date
283
+ @crc,
284
+ zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
285
+ zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
286
+ name_size,
287
+ @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
288
+ end
289
+
290
+ def write_local_entry(io, rewrite = false) #:nodoc:all
291
+ prep_zip64_extra(true)
292
+ verify_local_header_size! if rewrite
293
+ @local_header_offset = io.tell
294
+
295
+ io << pack_local_entry
296
+
297
+ io << @name
298
+ io << @extra.to_local_bin if @extra
299
+ @local_header_size = io.tell - @local_header_offset
300
+ end
301
+
302
+ def unpack_c_dir_entry(buf)
303
+ @header_signature,
304
+ @version, # version of encoding software
305
+ @fstype, # filesystem type
306
+ @version_needed_to_extract,
307
+ @gp_flags,
308
+ @compression_method,
309
+ @last_mod_time,
310
+ @last_mod_date,
311
+ @crc,
312
+ @compressed_size,
313
+ @size,
314
+ @name_length,
315
+ @extra_length,
316
+ @comment_length,
317
+ _, # diskNumberStart
318
+ @internal_file_attributes,
319
+ @external_file_attributes,
320
+ @local_header_offset,
321
+ @name,
322
+ @extra,
323
+ @comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
324
+ end
325
+
326
+ def set_ftype_from_c_dir_entry
327
+ @ftype = case @fstype
328
+ when ::Zip::FSTYPE_UNIX
329
+ @unix_perms = (@external_file_attributes >> 16) & 0o7777
330
+ case (@external_file_attributes >> 28)
331
+ when ::Zip::FILE_TYPE_DIR
332
+ :directory
333
+ when ::Zip::FILE_TYPE_FILE
334
+ :file
335
+ when ::Zip::FILE_TYPE_SYMLINK
336
+ :symlink
337
+ 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
340
+ if name_is_directory?
341
+ :directory
342
+ else
343
+ :file
344
+ end
345
+ end
346
+ else
347
+ if name_is_directory?
348
+ :directory
349
+ else
350
+ :file
351
+ end
352
+ end
353
+ end
354
+
355
+ def check_c_dir_entry_static_header_length(buf)
356
+ return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
357
+ raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
358
+ end
359
+
360
+ def check_c_dir_entry_signature
361
+ return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
362
+ raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
363
+ end
364
+
365
+ def check_c_dir_entry_comment_size
366
+ return if @comment && @comment.bytesize == @comment_length
367
+ raise ::Zip::Error, 'Truncated cdir zip entry header'
368
+ end
369
+
370
+ def read_c_dir_extra_field(io)
371
+ if @extra.is_a?(::Zip::ExtraField)
372
+ @extra.merge(io.read(@extra_length))
373
+ else
374
+ @extra = ::Zip::ExtraField.new(io.read(@extra_length))
375
+ end
376
+ end
377
+
378
+ def read_c_dir_entry(io) #:nodoc:all
379
+ static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
380
+ check_c_dir_entry_static_header_length(static_sized_fields_buf)
381
+ unpack_c_dir_entry(static_sized_fields_buf)
382
+ check_c_dir_entry_signature
383
+ set_time(@last_mod_date, @last_mod_time)
384
+ @name = io.read(@name_length)
385
+ if ::Zip.force_entry_names_encoding
386
+ @name.force_encoding(::Zip.force_entry_names_encoding)
387
+ end
388
+ read_c_dir_extra_field(io)
389
+ @comment = io.read(@comment_length)
390
+ check_c_dir_entry_comment_size
391
+ set_ftype_from_c_dir_entry
392
+ parse_zip64_extra(false)
393
+ end
394
+
395
+ def file_stat(path) # :nodoc:
396
+ if @follow_symlinks
397
+ ::File.stat(path)
398
+ else
399
+ ::File.lstat(path)
400
+ end
401
+ end
402
+
403
+ def get_extra_attributes_from_path(path) # :nodoc:
404
+ return if Zip::RUNNING_ON_WINDOWS
405
+ stat = file_stat(path)
406
+ @unix_uid = stat.uid
407
+ @unix_gid = stat.gid
408
+ @unix_perms = stat.mode & 0o7777
409
+ end
410
+
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()
419
+ end
420
+
421
+ def set_extra_attributes_on_path(dest_path) # :nodoc:
422
+ return unless file? || directory?
423
+
424
+ case @fstype
425
+ when ::Zip::FSTYPE_UNIX
426
+ set_unix_permissions_on_path(dest_path)
427
+ end
428
+ end
429
+
430
+ def pack_c_dir_entry
431
+ zip64 = @extra['Zip64']
432
+ [
433
+ @header_signature,
434
+ @version, # version of encoding software
435
+ @fstype, # filesystem type
436
+ @version_needed_to_extract, # @versionNeededToExtract
437
+ @gp_flags, # @gp_flags
438
+ @compression_method,
439
+ @time.to_binary_dos_time, # @last_mod_time
440
+ @time.to_binary_dos_date, # @last_mod_date
441
+ @crc,
442
+ zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
443
+ zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
444
+ name_size,
445
+ @extra ? @extra.c_dir_size : 0,
446
+ comment_size,
447
+ zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
448
+ @internal_file_attributes, # file type (binary=0, text=1)
449
+ @external_file_attributes, # native filesystem attributes
450
+ zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
451
+ @name,
452
+ @extra,
453
+ @comment
454
+ ].pack('VCCvvvvvVVVvvvvvVV')
455
+ end
456
+
457
+ def write_c_dir_entry(io) #:nodoc:all
458
+ prep_zip64_extra(false)
459
+ case @fstype
460
+ when ::Zip::FSTYPE_UNIX
461
+ ft = case @ftype
462
+ when :file
463
+ @unix_perms ||= 0o644
464
+ ::Zip::FILE_TYPE_FILE
465
+ when :directory
466
+ @unix_perms ||= 0o755
467
+ ::Zip::FILE_TYPE_DIR
468
+ when :symlink
469
+ @unix_perms ||= 0o755
470
+ ::Zip::FILE_TYPE_SYMLINK
471
+ end
472
+
473
+ unless ft.nil?
474
+ @external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
475
+ end
476
+ end
477
+
478
+ io << pack_c_dir_entry
479
+
480
+ io << @name
481
+ io << (@extra ? @extra.to_c_dir_bin : '')
482
+ io << @comment
483
+ end
484
+
485
+ def ==(other)
486
+ return false unless other.class == self.class
487
+ # Compares contents of local entry and exposed fields
488
+ keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
489
+ other.__send__(k.to_sym) == __send__(k.to_sym)
490
+ end
491
+ keys_equal && time.dos_equals(other.time)
492
+ end
493
+
494
+ def <=>(other)
495
+ to_s <=> other.to_s
496
+ end
497
+
498
+ # Returns an IO like object for the given ZipEntry.
499
+ # Warning: may behave weird with symlinks.
500
+ def get_input_stream(&block)
501
+ if @ftype == :directory
502
+ yield ::Zip::NullInputStream if block_given?
503
+ ::Zip::NullInputStream
504
+ elsif @filepath
505
+ case @ftype
506
+ when :file
507
+ ::File.open(@filepath, 'rb', &block)
508
+ when :symlink
509
+ linkpath = ::File.readlink(@filepath)
510
+ stringio = ::StringIO.new(linkpath)
511
+ yield(stringio) if block_given?
512
+ stringio
513
+ else
514
+ raise "unknown @file_type #{@ftype}"
515
+ end
516
+ else
517
+ zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
518
+ zis.instance_variable_set(:@complete_entry, self)
519
+ zis.get_next_entry
520
+ if block_given?
521
+ begin
522
+ yield(zis)
523
+ ensure
524
+ zis.close
525
+ end
526
+ else
527
+ zis
528
+ end
529
+ end
530
+ end
531
+
532
+ def gather_fileinfo_from_srcpath(src_path) # :nodoc:
533
+ stat = file_stat(src_path)
534
+ @ftype = case stat.ftype
535
+ when 'file'
536
+ if name_is_directory?
537
+ raise ArgumentError,
538
+ "entry name '#{newEntry}' indicates directory entry, but " \
539
+ "'#{src_path}' is not a directory"
540
+ end
541
+ :file
542
+ when 'directory'
543
+ @name += '/' unless name_is_directory?
544
+ :directory
545
+ when 'link'
546
+ if name_is_directory?
547
+ raise ArgumentError,
548
+ "entry name '#{newEntry}' indicates directory entry, but " \
549
+ "'#{src_path}' is not a directory"
550
+ end
551
+ :symlink
552
+ else
553
+ raise "unknown file type: #{src_path.inspect} #{stat.inspect}"
554
+ end
555
+
556
+ @filepath = src_path
557
+ get_extra_attributes_from_path(@filepath)
558
+ end
559
+
560
+ def write_to_zip_output_stream(zip_output_stream) #:nodoc:all
561
+ if @ftype == :directory
562
+ zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED)
563
+ 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) }
566
+ else
567
+ zip_output_stream.copy_raw_entry(self)
568
+ end
569
+ end
570
+
571
+ def parent_as_string
572
+ entry_name = name.chomp('/')
573
+ slash_index = entry_name.rindex('/')
574
+ slash_index ? entry_name.slice(0, slash_index + 1) : nil
575
+ end
576
+
577
+ def get_raw_input_stream(&block)
578
+ if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
579
+ yield @zipfile
580
+ else
581
+ ::File.open(@zipfile, 'rb', &block)
582
+ end
583
+ end
584
+
585
+ def clean_up
586
+ # By default, do nothing
587
+ end
588
+
589
+ private
590
+
591
+ def set_time(binary_dos_date, binary_dos_time)
592
+ @time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
593
+ rescue ArgumentError
594
+ warn 'Invalid date/time in zip entry' if ::Zip.warn_invalid_date
595
+ end
596
+
597
+ def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
598
+ if ::File.exist?(dest_path) && !yield(self, dest_path)
599
+ raise ::Zip::DestinationFileExistsError,
600
+ "Destination '#{dest_path}' already exists"
601
+ end
602
+ ::File.open(dest_path, 'wb') do |os|
603
+ get_input_stream do |is|
604
+ set_extra_attributes_on_path(dest_path)
605
+
606
+ bytes_written = 0
607
+ warned = false
608
+ buf = ''.dup
609
+ while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
610
+ os << buf
611
+ 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
621
+ end
622
+ end
623
+ end
624
+ end
625
+
626
+ def create_directory(dest_path)
627
+ return if ::File.directory?(dest_path)
628
+ 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
636
+ end
637
+ ::FileUtils.mkdir_p(dest_path)
638
+ set_extra_attributes_on_path(dest_path)
639
+ end
640
+
641
+ # BUG: create_symlink() does not use &block
642
+ def create_symlink(dest_path)
643
+ # TODO: Symlinks pose security challenges. Symlink support temporarily
644
+ # removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
645
+ puts "WARNING: skipped symlink #{dest_path}"
646
+ end
647
+
648
+ # apply missing data from the zip64 extra information field, if present
649
+ # (required when file sizes exceed 2**32, but can be used for all files)
650
+ def parse_zip64_extra(for_local_header) #:nodoc:all
651
+ return if @extra['Zip64'].nil?
652
+ if for_local_header
653
+ @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
654
+ else
655
+ @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(@size, @compressed_size, @local_header_offset)
656
+ end
657
+ end
658
+
659
+ def data_descriptor_size
660
+ (@gp_flags & 0x0008) > 0 ? 16 : 0
661
+ end
662
+
663
+ # create a zip64 extra information field if we need one
664
+ def prep_zip64_extra(for_local_header) #:nodoc:all
665
+ 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
669
+ @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')
684
+
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
693
+ end
694
+ end
695
+ end
696
+ end
697
+
698
+ # Copyright (C) 2002, 2003 Thomas Sondergaard
699
+ # rubyzip is free software; you can redistribute it and/or
700
+ # modify it under the terms of the ruby license.