ziperu 0.1.1-x86_64-linux

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE-rubyzip.txt +24 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +276 -0
  6. data/lib/rubyzip.rb +4 -0
  7. data/lib/zip/central_directory.rb +5 -0
  8. data/lib/zip/compressor.rb +5 -0
  9. data/lib/zip/constants.rb +5 -0
  10. data/lib/zip/crypto/aes_encryption.rb +5 -0
  11. data/lib/zip/crypto/decrypted_io.rb +5 -0
  12. data/lib/zip/crypto/encryption.rb +5 -0
  13. data/lib/zip/crypto/null_encryption.rb +5 -0
  14. data/lib/zip/crypto/traditional_encryption.rb +5 -0
  15. data/lib/zip/crypto.rb +5 -0
  16. data/lib/zip/decompressor.rb +5 -0
  17. data/lib/zip/deflater.rb +5 -0
  18. data/lib/zip/dirtyable.rb +5 -0
  19. data/lib/zip/dos_time.rb +5 -0
  20. data/lib/zip/entry.rb +5 -0
  21. data/lib/zip/entry_set.rb +5 -0
  22. data/lib/zip/errors.rb +5 -0
  23. data/lib/zip/extra_field/aes.rb +5 -0
  24. data/lib/zip/extra_field/generic.rb +5 -0
  25. data/lib/zip/extra_field/ntfs.rb +5 -0
  26. data/lib/zip/extra_field/old_unix.rb +5 -0
  27. data/lib/zip/extra_field/universal_time.rb +5 -0
  28. data/lib/zip/extra_field/unix.rb +5 -0
  29. data/lib/zip/extra_field/unknown.rb +5 -0
  30. data/lib/zip/extra_field/zip64.rb +5 -0
  31. data/lib/zip/extra_field.rb +5 -0
  32. data/lib/zip/file.rb +5 -0
  33. data/lib/zip/file_split.rb +5 -0
  34. data/lib/zip/filesystem/dir.rb +5 -0
  35. data/lib/zip/filesystem/directory_iterator.rb +5 -0
  36. data/lib/zip/filesystem/file.rb +5 -0
  37. data/lib/zip/filesystem/file_stat.rb +5 -0
  38. data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
  39. data/lib/zip/filesystem.rb +5 -0
  40. data/lib/zip/inflater.rb +5 -0
  41. data/lib/zip/input_stream.rb +5 -0
  42. data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
  43. data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
  44. data/lib/zip/ioextras/fake_io.rb +5 -0
  45. data/lib/zip/ioextras.rb +5 -0
  46. data/lib/zip/null_compressor.rb +5 -0
  47. data/lib/zip/null_decompressor.rb +5 -0
  48. data/lib/zip/null_input_stream.rb +5 -0
  49. data/lib/zip/output_stream.rb +5 -0
  50. data/lib/zip/pass_thru_compressor.rb +5 -0
  51. data/lib/zip/pass_thru_decompressor.rb +5 -0
  52. data/lib/zip/streamable_directory.rb +5 -0
  53. data/lib/zip/streamable_stream.rb +5 -0
  54. data/lib/zip/version.rb +5 -0
  55. data/lib/zip.rb +19 -0
  56. data/lib/ziperu/3.1/ziperu.so +0 -0
  57. data/lib/ziperu/3.2/ziperu.so +0 -0
  58. data/lib/ziperu/3.3/ziperu.so +0 -0
  59. data/lib/ziperu/3.4/ziperu.so +0 -0
  60. data/lib/ziperu/4.0/ziperu.so +0 -0
  61. data/lib/ziperu/central_directory.rb +136 -0
  62. data/lib/ziperu/compressor.rb +14 -0
  63. data/lib/ziperu/constants.rb +148 -0
  64. data/lib/ziperu/crypto.rb +72 -0
  65. data/lib/ziperu/decompressor.rb +35 -0
  66. data/lib/ziperu/deflater.rb +41 -0
  67. data/lib/ziperu/dirtyable.rb +48 -0
  68. data/lib/ziperu/dos_time.rb +87 -0
  69. data/lib/ziperu/entry.rb +760 -0
  70. data/lib/ziperu/entry_set.rb +147 -0
  71. data/lib/ziperu/errors.rb +154 -0
  72. data/lib/ziperu/extra_field/aes.rb +52 -0
  73. data/lib/ziperu/extra_field/generic.rb +45 -0
  74. data/lib/ziperu/extra_field/ntfs.rb +98 -0
  75. data/lib/ziperu/extra_field/old_unix.rb +52 -0
  76. data/lib/ziperu/extra_field/universal_time.rb +81 -0
  77. data/lib/ziperu/extra_field/unix.rb +45 -0
  78. data/lib/ziperu/extra_field/unknown.rb +37 -0
  79. data/lib/ziperu/extra_field/zip64.rb +86 -0
  80. data/lib/ziperu/extra_field.rb +109 -0
  81. data/lib/ziperu/file.rb +614 -0
  82. data/lib/ziperu/file_split.rb +93 -0
  83. data/lib/ziperu/filesystem/dir.rb +88 -0
  84. data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
  85. data/lib/ziperu/filesystem/file.rb +265 -0
  86. data/lib/ziperu/filesystem/file_stat.rb +112 -0
  87. data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
  88. data/lib/ziperu/filesystem.rb +76 -0
  89. data/lib/ziperu/inflater.rb +60 -0
  90. data/lib/ziperu/input_stream.rb +296 -0
  91. data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
  92. data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
  93. data/lib/ziperu/ioextras/fake_io.rb +41 -0
  94. data/lib/ziperu/ioextras.rb +42 -0
  95. data/lib/ziperu/null_compressor.rb +20 -0
  96. data/lib/ziperu/null_decompressor.rb +25 -0
  97. data/lib/ziperu/null_input_stream.rb +55 -0
  98. data/lib/ziperu/output_stream.rb +223 -0
  99. data/lib/ziperu/pass_thru_compressor.rb +29 -0
  100. data/lib/ziperu/pass_thru_decompressor.rb +37 -0
  101. data/lib/ziperu/streamable_directory.rb +15 -0
  102. data/lib/ziperu/streamable_stream.rb +59 -0
  103. data/lib/ziperu/version.rb +7 -0
  104. data/lib/ziperu.rb +116 -0
  105. metadata +156 -0
@@ -0,0 +1,760 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Ziperu
6
+ # Ziperu::Entry represents an entry in a Zip archive. The public surface
7
+ # (constructor, accessors, `time`, `ftype`, `==`) matches rubyzip's; entries
8
+ # read from an archive are built from the Hash the native core returns and
9
+ # consult the archive by index only for bytes.
10
+ class Entry
11
+ include Dirtyable
12
+
13
+ # Constant used to specify that the entry is stored (i.e., not compressed).
14
+ STORED = ::Ziperu::COMPRESSION_METHOD_STORE
15
+
16
+ # Constant used to specify that the entry is deflated (i.e., compressed).
17
+ DEFLATED = ::Ziperu::COMPRESSION_METHOD_DEFLATE
18
+
19
+ # Two more methods the native writer can produce. rubyzip cannot read
20
+ # either, nor can Windows Explorer or Java; 7-Zip, Info-ZIP and
21
+ # libarchive can.
22
+ BZIP2 = ::Ziperu::COMPRESSION_METHOD_BZIP2
23
+ ZSTD = ::Ziperu::COMPRESSION_METHOD_ZSTD
24
+
25
+ # Language encoding flag (EFS) bit
26
+ EFS = 0b100000000000 # :nodoc:
27
+
28
+ # Compression level flags (bits 1 and 2 of the gp flags).
29
+ COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110 # :nodoc:
30
+ COMPRESSION_LEVEL_FAST_GPFLAG = 0b100 # :nodoc:
31
+ COMPRESSION_LEVEL_MAX_GPFLAG = 0b010 # :nodoc:
32
+
33
+ attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
34
+ :restore_ownership, :restore_permissions, :restore_times,
35
+ :unix_gid, :unix_perms, :unix_uid
36
+
37
+ attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
38
+ :internal_file_attributes, :local_header_offset # :nodoc:
39
+
40
+ attr_reader :compression_level, :filepath # :nodoc:
41
+
42
+ # The entry's extra fields. For an entry read from an archive, the
43
+ # local header's fields (which is where Info-ZIP unix uid/gid and the
44
+ # extended timestamp's atime/ctime live) are merged in on first use,
45
+ # rather than by seeking to every local header when the archive opens.
46
+ def extra
47
+ if @archive && !@local_extra_merged
48
+ @local_extra_merged = true
49
+ local = @index ? archive.local_extra(@index) : archive.local_extra_at(@local_header_offset)
50
+ @extra.merge(local, local: true)
51
+ end
52
+ @extra
53
+ end
54
+
55
+ # The native archive this entry was read from, reopened if the owning
56
+ # Ziperu::File has released it (rubyzip lets entries be read after `close`).
57
+ def archive # :nodoc:
58
+ if @archive&.closed? && @zipfile.kind_of?(::Ziperu::File)
59
+ reopened = @zipfile.native_archive
60
+ unless still_at_place?(reopened)
61
+ where = @index ? "at index #{@index} of" : 'in'
62
+ raise Error, "entry '#{@original_name}' is no longer #{where} #{@zipfile.name}"
63
+ end
64
+
65
+ @archive = reopened
66
+ end
67
+ @archive
68
+ end
69
+
70
+ attr_writer :size # :nodoc:
71
+
72
+ # Index of the entry in the archive's central directory, or nil for an
73
+ # entry that is not (yet) in an archive, or that the native core cannot
74
+ # address by index: one of several records under the same name, which
75
+ # is read from its local header offset instead.
76
+ attr_reader :index # :nodoc:
77
+
78
+ mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
79
+ :fstype=, :gp_flags=, :name=, :size=,
80
+ :unix_gid=, :unix_perms=, :unix_uid=
81
+
82
+ # Create a new Ziperu::Entry.
83
+ def initialize(
84
+ zipfile = '', name = '',
85
+ comment: '', size: nil, compressed_size: 0, crc: 0,
86
+ compression_method: DEFLATED,
87
+ compression_level: ::Ziperu.default_compression,
88
+ time: ::Ziperu::DOSTime.now, extra: ::Ziperu::ExtraField.new
89
+ )
90
+ super()
91
+ @name = name
92
+ check_name(@name)
93
+
94
+ set_default_vars_values
95
+ @fstype = ::Ziperu::RUNNING_ON_WINDOWS ? ::Ziperu::FSTYPE_FAT : ::Ziperu::FSTYPE_UNIX
96
+
97
+ @zipfile = zipfile
98
+ @archive = nil
99
+ @index = nil
100
+ @original_name = nil
101
+ @comment = comment || ''
102
+ @compression_method = compression_method || DEFLATED
103
+ @compression_level = compression_level || ::Ziperu.default_compression
104
+ @compressed_size = compressed_size || 0
105
+ @crc = crc || 0
106
+ @size = size
107
+ @time = case time
108
+ when ::Ziperu::DOSTime
109
+ time
110
+ when Time
111
+ ::Ziperu::DOSTime.from_time(time)
112
+ else
113
+ ::Ziperu::DOSTime.now
114
+ end
115
+ @extra =
116
+ extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
117
+
118
+ set_compression_level_flags
119
+ end
120
+
121
+ # Build an entry from the metadata Hash the native core produces for
122
+ # one central-directory record.
123
+ #
124
+ # @api private
125
+ def self.from_meta(archive, meta, zipfile = nil)
126
+ entry = allocate
127
+ entry.send(:init_from_meta, archive, meta, zipfile)
128
+ entry
129
+ end
130
+
131
+ # Is this entry encrypted?
132
+ def encrypted?
133
+ gp_flags & 1 == 1
134
+ end
135
+
136
+ # Was this entry written in streaming mode with sizes still unknown?
137
+ # Only informational here: reading goes through the central directory,
138
+ # so an incomplete local header does not stop `get_input_stream`.
139
+ def incomplete? # :nodoc:
140
+ (gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
141
+ end
142
+
143
+ # Was a size given, or is it still unknown (as for an entry that is
144
+ # about to be streamed)? Decides whether a zip64 placeholder is needed.
145
+ def size_known? # :nodoc:
146
+ !@size.nil?
147
+ end
148
+
149
+ # The uncompressed size of the entry.
150
+ def size
151
+ @size || 0
152
+ end
153
+
154
+ # Get a timestamp component of this entry. Returns modification time by
155
+ # default.
156
+ def time(component: :mtime)
157
+ time =
158
+ if extra[:universaltime]
159
+ extra[:universaltime].send(component)
160
+ elsif extra[:ntfs]
161
+ extra[:ntfs].send(component)
162
+ end
163
+
164
+ time || (@time if component == :mtime)
165
+ end
166
+
167
+ alias mtime time
168
+
169
+ def atime
170
+ time(component: :atime)
171
+ end
172
+
173
+ def ctime
174
+ time(component: :ctime)
175
+ end
176
+
177
+ def time=(value, component: :mtime)
178
+ mark_dirty!(:time=)
179
+ unless extra.member?(:universaltime) || extra.member?(:ntfs)
180
+ extra.create(:universaltime)
181
+ end
182
+
183
+ value = DOSTime.from_time(value)
184
+ comp = "#{component}=" unless component.to_s.end_with?('=')
185
+ (extra[:universaltime] || extra[:ntfs]).send(comp, value)
186
+ @time = value if component == :mtime
187
+ end
188
+
189
+ alias mtime= time=
190
+
191
+ def atime=(value)
192
+ send(:time=, value, component: :atime)
193
+ end
194
+
195
+ def ctime=(value)
196
+ send(:time=, value, component: :ctime)
197
+ end
198
+
199
+ # Does this entry return time fields with accurate timezone information?
200
+ def absolute_time?
201
+ extra.member?(:universaltime) || extra.member?(:ntfs)
202
+ end
203
+
204
+ # Returns STORED if the entry is a directory or the compression level is 0.
205
+ def compression_method
206
+ return STORED if ftype == :directory || @compression_level == 0
207
+
208
+ @compression_method
209
+ end
210
+
211
+ def compression_method=(method)
212
+ @dirty = true
213
+ @compression_method = (ftype == :directory ? STORED : method)
214
+ end
215
+
216
+ def zip64?
217
+ !extra[:zip64].nil?
218
+ end
219
+
220
+ def aes?
221
+ !extra[:aes].nil?
222
+ end
223
+
224
+ def file_type_is?(type) # :nodoc:
225
+ ftype == type
226
+ end
227
+
228
+ def ftype # :nodoc:
229
+ @ftype ||= name_is_directory? ? :directory : :file
230
+ end
231
+
232
+ %w[directory file symlink].each do |k|
233
+ define_method :"#{k}?" do
234
+ file_type_is?(k.to_sym)
235
+ end
236
+ end
237
+
238
+ def name_is_directory? # :nodoc:
239
+ @name.end_with?('/')
240
+ end
241
+
242
+ # Is the name a relative path, free of `..` patterns that could lead to
243
+ # path traversal attacks? This does NOT handle symlinks.
244
+ def name_safe? # :nodoc:
245
+ cleanpath = Pathname.new(@name).cleanpath
246
+ return false unless cleanpath.relative?
247
+
248
+ root = ::File::SEPARATOR
249
+ naive = Regexp.escape(::File.join(root, cleanpath.to_s))
250
+ ::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
251
+ end
252
+
253
+ alias header_offset local_header_offset
254
+
255
+ # What the entry came to once written: the stream's `close` fills these
256
+ # in so that a manifest built from the entries just written is right.
257
+ def written_as(header_offset, size, compressed_size, crc) # :nodoc:
258
+ @local_header_offset = header_offset
259
+ @size = size
260
+ @compressed_size = compressed_size
261
+ @crc = crc
262
+ end
263
+
264
+ # Entries serialize without their archive handle; a loaded one can be
265
+ # inspected but not read.
266
+ def marshal_dump # :nodoc:
267
+ instance_variables.each_with_object({}) do |ivar, h|
268
+ next if ivar == :@archive
269
+ next if ivar == :@zipfile && @zipfile.kind_of?(::Ziperu::File)
270
+
271
+ h[ivar] = instance_variable_get(ivar)
272
+ end
273
+ end
274
+
275
+ def marshal_load(ivars) # :nodoc:
276
+ ivars.each { |ivar, value| instance_variable_set(ivar, value) }
277
+ @archive = nil
278
+ @zipfile = nil unless instance_variable_defined?(:@zipfile)
279
+ end
280
+
281
+ def to_s # :nodoc:
282
+ @name
283
+ end
284
+
285
+ def ==(other) # :nodoc:
286
+ return false unless other.class == self.class
287
+
288
+ %w[compression_method crc compressed_size size name extra filepath time].all? do |k|
289
+ other.__send__(k.to_sym) == __send__(k.to_sym)
290
+ end
291
+ end
292
+
293
+ def <=>(other) # :nodoc:
294
+ to_s <=> other.to_s
295
+ end
296
+
297
+ # Extracts this entry to a file at `entry_path`, with
298
+ # `destination_directory` as the base location in the filesystem.
299
+ #
300
+ # NB: The caller is responsible for making sure `destination_directory` is
301
+ # safe, if it is passed.
302
+ #
303
+ # `create_parent_directories`, if true, creates any missing intermediate
304
+ # directories for a file entry before writing it. This defaults to
305
+ # false: a missing intermediate directory usually means an earlier
306
+ # entry (e.g. a symlink) was deliberately skipped as unsafe, and letting
307
+ # a later entry silently succeed by re-creating that path as a plain
308
+ # directory would mask that. `File#extract_all` opts into it.
309
+ #
310
+ # A symlink entry is skipped with a warning unless `symlinks: :create`,
311
+ # which creates it only when its target resolves to a path inside
312
+ # `destination_directory`.
313
+ def extract(entry_path = @name, destination_directory: '.',
314
+ create_parent_directories: false, symlinks: :skip, &block)
315
+ dest_dir = ::File.absolute_path(destination_directory || '.')
316
+ extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
317
+
318
+ unless extract_path.start_with?(dest_dir + ::File::SEPARATOR) || dest_dir == ::File::SEPARATOR
319
+ warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
320
+ return self
321
+ end
322
+
323
+ block ||= proc { ::Ziperu.on_exists_proc }
324
+
325
+ raise "unknown file type #{inspect}" unless directory? || file? || symlink?
326
+
327
+ ::FileUtils.mkdir_p(::File.dirname(extract_path)) if create_parent_directories && !directory?
328
+
329
+ if symlink?
330
+ create_symlink(extract_path, dest_dir, symlinks, &block)
331
+ else
332
+ __send__(:"create_#{ftype}", extract_path, &block)
333
+ end
334
+ self
335
+ end
336
+
337
+ # Where a symlink entry points: the link on disk for one being added,
338
+ # the stored content for one read from an archive.
339
+ def symlink_target # :nodoc:
340
+ return ::File.readlink(@filepath) if @filepath
341
+
342
+ get_raw_input_string
343
+ end
344
+
345
+ # Returns an IO like object for the given ZipEntry.
346
+ # Optional decrypter can be provided for an encrypted entry
347
+ # Warning: may behave weird with symlinks.
348
+ def get_input_stream(decrypter: nil, &block)
349
+ return open_input_stream(decrypter: decrypter, &block) if @filepath.nil? || ftype == :directory
350
+
351
+ # Not yet in an archive: the data is a file on disk.
352
+ case ftype
353
+ when :file
354
+ ::File.open(@filepath, 'rb', &block)
355
+ when :symlink
356
+ stringio = ::StringIO.new(::File.readlink(@filepath))
357
+ yield(stringio) if block
358
+ stringio
359
+ else
360
+ raise "unknown @file_type #{ftype}"
361
+ end
362
+ end
363
+
364
+ def gather_fileinfo_from_srcpath(src_path) # :nodoc:
365
+ stat = file_stat(src_path)
366
+ @ftype = case stat.ftype
367
+ when 'file'
368
+ if name_is_directory?
369
+ raise ArgumentError,
370
+ "entry name '#{@name}' indicates a directory entry, but " \
371
+ "'#{src_path}' is not a directory"
372
+ end
373
+ :file
374
+ when 'directory'
375
+ @name += '/' unless name_is_directory?
376
+ :directory
377
+ when 'link'
378
+ if name_is_directory?
379
+ raise ArgumentError,
380
+ "entry name '#{@name}' indicates a directory entry, but " \
381
+ "'#{src_path}' is not a directory"
382
+ end
383
+ :symlink
384
+ else
385
+ raise "unknown file type: #{src_path.inspect} #{stat.inspect}"
386
+ end
387
+
388
+ @filepath = src_path
389
+ @size = stat.size
390
+ get_extra_attributes_from_path(@filepath)
391
+ end
392
+
393
+ # Writes this entry (header and data) to an OutputStream. An entry read
394
+ # from an archive is copied without recompression when it is unchanged,
395
+ # or when its only change is one the native raw copy can carry: a new
396
+ # name, or new permissions. Any other edit (comment, time, extra
397
+ # fields, several at once) is written by re-streaming the data under a
398
+ # fresh header, as rubyzip's rewrite of every header would. Entries
399
+ # that cannot be re-streamed (encrypted, or in a method the writer
400
+ # lacks) fall back to the raw copy, so only the rename or the
401
+ # permissions persist for them.
402
+ def write_to_zip_output_stream(zip_output_stream) # :nodoc:
403
+ if ftype == :directory || (ftype == :symlink && (@filepath || needs_restream?))
404
+ # No data to stream: a symlink's target goes into the header call.
405
+ zip_output_stream.put_next_entry(self)
406
+ elsif @filepath || needs_restream? || no_raw_copy?
407
+ zip_output_stream.put_next_entry(self)
408
+ get_input_stream do |is|
409
+ ::Ziperu::IOExtras.copy_stream(zip_output_stream, is)
410
+ end
411
+ else
412
+ renamed = @name.b != @original_name.b.tr('\\', '/')
413
+ zip_output_stream.copy_raw_entry(self, (@name if renamed), touch: dirty? && !renamed)
414
+ end
415
+ end
416
+
417
+ # What `FileSystem#chmod` sets, and what a touch on the raw copy writes.
418
+ PERMISSION_FIELDS = %i[unix_perms= external_file_attributes= fstype=].freeze # :nodoc:
419
+
420
+ # Extra fields the native raw copy does not carry over intact: it
421
+ # re-emits the central directory's form of each field in both headers,
422
+ # which drops the extended timestamp and NTFS fields and leaves the
423
+ # Info-ZIP unix fields without their uid/gid.
424
+ RAW_COPY_LOSES = %i[universaltime ntfs oldunix iunix].freeze # :nodoc:
425
+
426
+ def raw_copy_carries_changes? # :nodoc:
427
+ dirty_fields == [:name=] || (dirty_fields - PERMISSION_FIELDS).empty?
428
+ end
429
+
430
+ # Whether the entry has to be decompressed and written afresh rather
431
+ # than copied byte for byte. An encrypted entry cannot be: edits to it
432
+ # beyond a rename or chmod raise rather than vanish on commit, and the
433
+ # extra fields the raw copy loses are lost.
434
+ def needs_restream? # :nodoc:
435
+ if dirty? && !raw_copy_carries_changes?
436
+ unless restreamable?
437
+ changed = dirty_fields.map { |f| f.to_s.chomp('=') }.join(', ')
438
+ kind = encrypted? ? 'an encrypted' : "a #{COMPRESSION_METHODS[compression_method]}"
439
+ raise Error, "cannot change #{changed} of entry '#{@name}': #{kind} entry " \
440
+ 'can only be copied as is (a rename or chmod is fine)'
441
+ end
442
+
443
+ return true
444
+ end
445
+
446
+ restreamable? && (extra.keys & RAW_COPY_LOSES).any?
447
+ end
448
+
449
+ def restreamable? # :nodoc:
450
+ !encrypted? && ::Ziperu::WRITABLE_COMPRESSION_METHODS.include?(compression_method)
451
+ end
452
+
453
+ # A record the native core cannot address by index (one of several
454
+ # under the same name) has no raw copy: it is written afresh, which
455
+ # needs a method the writer has and no encryption.
456
+ def no_raw_copy? # :nodoc:
457
+ return false unless @index.nil?
458
+ return true if restreamable?
459
+
460
+ kind = encrypted? ? 'an encrypted' : "a #{COMPRESSION_METHODS[compression_method]}"
461
+ raise Error, "cannot write entry '#{@name}': #{kind} entry that shares its name with " \
462
+ 'another record can only be read'
463
+ end
464
+
465
+ # Whether the reopened archive still holds this entry where it was.
466
+ def still_at_place?(reopened) # :nodoc:
467
+ name = @original_name.b
468
+ return reopened.entry_meta(@index)[:name] == name if @index
469
+
470
+ reopened.entries.any? do |meta|
471
+ meta[:index].nil? && meta[:header_offset] == @local_header_offset && meta[:name] == name
472
+ end
473
+ end
474
+
475
+ def parent_as_string # :nodoc:
476
+ entry_name = name.chomp('/')
477
+ slash_index = entry_name.rindex('/')
478
+ slash_index ? entry_name.slice(0, slash_index + 1) : nil
479
+ end
480
+
481
+ def clean_up # :nodoc:
482
+ @dirty = false # Any changes are written at this point.
483
+ @dirty_fields = []
484
+ end
485
+
486
+ def file_stat(path) # :nodoc:
487
+ if @follow_symlinks
488
+ ::File.stat(path)
489
+ else
490
+ ::File.lstat(path)
491
+ end
492
+ end
493
+
494
+ def get_extra_attributes_from_path(path) # :nodoc:
495
+ stat = file_stat(path)
496
+ @time = DOSTime.from_time(stat.mtime)
497
+ return if ::Ziperu::RUNNING_ON_WINDOWS
498
+
499
+ @unix_uid = stat.uid
500
+ @unix_gid = stat.gid
501
+ @unix_perms = stat.mode & 0o7777
502
+ end
503
+
504
+ # Decompressed contents as a binary String, in one native call.
505
+ def get_raw_input_string # :nodoc:
506
+ return ''.b if ftype == :directory
507
+ raise Error, "entry '#{@name}' is not in an archive" if archive.nil?
508
+ # A record without an index is read through the stream.
509
+ return open_input_stream(enforce_size: true, &:read) if @index.nil?
510
+
511
+ archive.read_entry(@index)
512
+ rescue Native::SizeLimitError
513
+ raise EntrySizeError, self
514
+ end
515
+
516
+ # The whole entry as a binary String, the way `Ziperu::File#read` reads
517
+ # one: in a single native call when the entry is in an archive and its
518
+ # declared size is to be trusted, through a stream otherwise.
519
+ def read_all # :nodoc:
520
+ if @filepath.nil? && @archive && !encrypted? && ::Ziperu.validate_entry_sizes
521
+ get_raw_input_string
522
+ else
523
+ get_input_stream(&:read)
524
+ end
525
+ end
526
+
527
+ def inspect
528
+ "#<#{self.class.name}:#{name} size=#{size} crc=#{format('%08x', crc)}>"
529
+ end
530
+
531
+ private
532
+
533
+ # Like get_input_stream, with `enforce_size: false` reading past a
534
+ # declared size that is too small (what rubyzip does when
535
+ # Ziperu.validate_entry_sizes is off) instead of stopping at it.
536
+ def open_input_stream(decrypter: nil, enforce_size: ::Ziperu.validate_entry_sizes)
537
+ if ftype == :directory
538
+ return yield(::Ziperu::NullInputStream) if block_given?
539
+
540
+ return ::Ziperu::NullInputStream
541
+ end
542
+
543
+ raise Error, "entry '#{@name}' is not in an archive" if @archive.nil?
544
+
545
+ # No offset: get_next_stream_entry positions by index, and resolving
546
+ # an offset would walk the whole central directory on every read.
547
+ zis = ::Ziperu::InputStream.new(archive, decrypter: decrypter, enforce_size: enforce_size)
548
+ zis.get_next_stream_entry(self)
549
+ return zis unless block_given?
550
+
551
+ begin
552
+ yield zis
553
+ ensure
554
+ zis.close
555
+ end
556
+ end
557
+
558
+ def create_file(dest_path, _continue_on_exists_proc = proc { Ziperu.continue_on_exists_proc })
559
+ if ::File.exist?(dest_path) && !yield(self, dest_path)
560
+ raise DestinationExistsError, dest_path
561
+ end
562
+
563
+ ::File.open(dest_path, 'wb') do |os|
564
+ open_input_stream(enforce_size: Ziperu.validate_entry_sizes) do |is|
565
+ bytes_written = 0
566
+ while (buf = is.sysread(IOExtras::CHUNK_SIZE))
567
+ os << buf
568
+ bytes_written += buf.bytesize
569
+ end
570
+ # With enforcement on, the native reader has already raised
571
+ # EntrySizeError; without it, warn as rubyzip does.
572
+ warn "WARNING: #{EntrySizeError.new(self).message}" if bytes_written > size
573
+ end
574
+ end
575
+
576
+ set_extra_attributes_on_path(dest_path)
577
+ end
578
+
579
+ def create_directory(dest_path)
580
+ return if ::File.directory?(dest_path)
581
+
582
+ if ::File.exist?(dest_path)
583
+ raise ::Ziperu::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
584
+
585
+ ::FileUtils.rm_f dest_path
586
+ end
587
+
588
+ ::FileUtils.mkdir_p(dest_path)
589
+ set_extra_attributes_on_path(dest_path)
590
+ end
591
+
592
+ # Symlinks are never created on extraction: a link pointing outside the
593
+ # destination would let a later entry write through it.
594
+ def create_symlink(dest_path, dest_dir, mode)
595
+ unless mode == :create
596
+ warn "WARNING: skipped symlink '#{dest_path}'."
597
+ return
598
+ end
599
+
600
+ target = symlink_target
601
+ resolved = ::File.expand_path(target, ::File.dirname(dest_path))
602
+ unless resolved.start_with?(dest_dir + ::File::SEPARATOR) || resolved == dest_dir
603
+ warn "WARNING: skipped symlink '#{@name}' to '#{target}' as it points outside the destination."
604
+ return
605
+ end
606
+
607
+ if ::File.symlink?(dest_path) || ::File.exist?(dest_path)
608
+ raise DestinationExistsError, dest_path unless yield(self, dest_path)
609
+
610
+ ::File.unlink(dest_path)
611
+ end
612
+ ::File.symlink(target, dest_path)
613
+ rescue NotImplementedError, Errno::EACCES, Errno::EPERM => e
614
+ warn "WARNING: could not create symlink '#{dest_path}': #{e.message}"
615
+ end
616
+
617
+ def set_unix_attributes_on_path(dest_path) # :nodoc:
618
+ # Ignore setuid/setgid bits by default. Honour if @restore_ownership.
619
+ unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
620
+ if @restore_permissions && @unix_perms
621
+ ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
622
+ end
623
+ return unless @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
624
+
625
+ ::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
626
+ end
627
+
628
+ def set_extra_attributes_on_path(dest_path) # :nodoc:
629
+ return unless file? || directory?
630
+
631
+ set_unix_attributes_on_path(dest_path) if @fstype == ::Ziperu::FSTYPE_UNIX
632
+
633
+ # Restore the timestamp on a file. This will either have come from the
634
+ # original source file that was copied into the archive, or from the
635
+ # creation date of the archive if there was no original source file.
636
+ ::FileUtils.touch(dest_path, mtime: time) if @restore_times
637
+ end
638
+
639
+ def set_default_vars_values # :nodoc:
640
+ @local_header_offset = 0
641
+ @internal_file_attributes = 1
642
+ @external_file_attributes = 0
643
+
644
+ @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT
645
+ @version = VERSION_MADE_BY
646
+
647
+ @ftype = nil
648
+ @filepath = nil
649
+ @gp_flags = 0
650
+ if ::Ziperu.unicode_names
651
+ @gp_flags |= EFS
652
+ @version = 63
653
+ end
654
+ @follow_symlinks = false
655
+
656
+ @restore_times = DEFAULT_RESTORE_OPTIONS[:restore_times]
657
+ @restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
658
+ @restore_ownership = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
659
+ @unix_uid = nil
660
+ @unix_gid = nil
661
+ @unix_perms = nil
662
+ end
663
+
664
+ def check_name(name) # :nodoc:
665
+ raise EntryNameError, name if name.start_with?('/')
666
+ raise EntryNameError if name.length > 65_535
667
+ end
668
+
669
+ def init_from_meta(archive, meta, zipfile)
670
+ @dirty = false
671
+ set_default_vars_values
672
+ @zipfile = zipfile
673
+ @archive = archive
674
+ @index = meta[:index]
675
+ # Names and comments are the stored bytes. They become UTF-8 when the
676
+ # entry's language-encoding flag says so (rubyzip leaves them binary),
677
+ # or whatever `Ziperu.force_entry_names_encoding` names; otherwise
678
+ # they stay binary. A lookup finds a name in any encoding with the
679
+ # same bytes. The raw name, before separators are normalised, is what
680
+ # the archive knows the entry by.
681
+ @gp_flags = meta[:flags]
682
+ @original_name = meta[:name].dup
683
+ @name = decode_stored_text(@original_name.dup)
684
+ @name.tr!('\\', '/')
685
+ @size = meta[:size]
686
+ @compressed_size = meta[:compressed_size]
687
+ @crc = meta[:crc]
688
+ @compression_method = meta[:compression_method]
689
+ @compression_level = ::Ziperu.default_compression
690
+ @fstype = meta[:fstype]
691
+ @version = meta[:version_made_by]
692
+ @external_file_attributes = meta[:external_attributes]
693
+ set_time(meta[:dos_date], meta[:dos_time])
694
+ @comment = decode_stored_text(meta[:comment].dup)
695
+ @extra = ExtraField.new(meta[:extra])
696
+ @local_extra_merged = false
697
+ @local_header_offset = meta[:header_offset]
698
+ set_ftype_from_c_dir_entry
699
+ end
700
+
701
+ # Stored bytes as a String in the encoding the entry declares.
702
+ def decode_stored_text(text) # :nodoc:
703
+ forced = ::Ziperu.force_entry_names_encoding
704
+ if forced
705
+ text.force_encoding(forced)
706
+ elsif (@gp_flags & EFS).nonzero?
707
+ text.force_encoding(::Encoding::UTF_8)
708
+ text.force_encoding(::Encoding::BINARY) unless text.valid_encoding?
709
+ end
710
+ text
711
+ end
712
+
713
+ # Like rubyzip, an unparseable DOS timestamp is reported (when
714
+ # `Ziperu.warn_invalid_date` is set) and the entry keeps "now".
715
+ def set_time(binary_dos_date, binary_dos_time)
716
+ @time = ::Ziperu::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
717
+ rescue ArgumentError
718
+ @time = ::Ziperu::DOSTime.now
719
+ warn 'WARNING: invalid date/time in zip entry.' if ::Ziperu.warn_invalid_date
720
+ end
721
+
722
+ def set_compression_level_flags
723
+ return unless compression_method == DEFLATED
724
+
725
+ case @compression_level
726
+ when 1
727
+ @gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
728
+ when 2
729
+ @gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
730
+ when 8, 9
731
+ @gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
732
+ end
733
+ end
734
+
735
+ def set_ftype_from_c_dir_entry # :nodoc:
736
+ @ftype = case @fstype
737
+ when ::Ziperu::FSTYPE_UNIX
738
+ @unix_perms = (@external_file_attributes >> 16) & 0o7777
739
+ case (@external_file_attributes >> 28)
740
+ when ::Ziperu::FILE_TYPE_DIR
741
+ :directory
742
+ when ::Ziperu::FILE_TYPE_FILE
743
+ :file
744
+ when ::Ziperu::FILE_TYPE_SYMLINK
745
+ :symlink
746
+ else
747
+ name_is_directory? ? :directory : :file
748
+ end
749
+ else
750
+ # Not a Unix host, so rubyzip ignores the high attribute
751
+ # bits. The native writer stamps the host of the machine it
752
+ # runs on, so archives written here on Windows would lose
753
+ # their permissions on reread; a non-zero mode is kept.
754
+ perms = (@external_file_attributes >> 16) & 0o7777
755
+ @unix_perms = perms unless perms.zero?
756
+ name_is_directory? ? :directory : :file
757
+ end
758
+ end
759
+ end
760
+ end