ziperu 0.1.1-aarch64-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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE-rubyzip.txt +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +276 -0
- data/lib/rubyzip.rb +4 -0
- data/lib/zip/central_directory.rb +5 -0
- data/lib/zip/compressor.rb +5 -0
- data/lib/zip/constants.rb +5 -0
- data/lib/zip/crypto/aes_encryption.rb +5 -0
- data/lib/zip/crypto/decrypted_io.rb +5 -0
- data/lib/zip/crypto/encryption.rb +5 -0
- data/lib/zip/crypto/null_encryption.rb +5 -0
- data/lib/zip/crypto/traditional_encryption.rb +5 -0
- data/lib/zip/crypto.rb +5 -0
- data/lib/zip/decompressor.rb +5 -0
- data/lib/zip/deflater.rb +5 -0
- data/lib/zip/dirtyable.rb +5 -0
- data/lib/zip/dos_time.rb +5 -0
- data/lib/zip/entry.rb +5 -0
- data/lib/zip/entry_set.rb +5 -0
- data/lib/zip/errors.rb +5 -0
- data/lib/zip/extra_field/aes.rb +5 -0
- data/lib/zip/extra_field/generic.rb +5 -0
- data/lib/zip/extra_field/ntfs.rb +5 -0
- data/lib/zip/extra_field/old_unix.rb +5 -0
- data/lib/zip/extra_field/universal_time.rb +5 -0
- data/lib/zip/extra_field/unix.rb +5 -0
- data/lib/zip/extra_field/unknown.rb +5 -0
- data/lib/zip/extra_field/zip64.rb +5 -0
- data/lib/zip/extra_field.rb +5 -0
- data/lib/zip/file.rb +5 -0
- data/lib/zip/file_split.rb +5 -0
- data/lib/zip/filesystem/dir.rb +5 -0
- data/lib/zip/filesystem/directory_iterator.rb +5 -0
- data/lib/zip/filesystem/file.rb +5 -0
- data/lib/zip/filesystem/file_stat.rb +5 -0
- data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
- data/lib/zip/filesystem.rb +5 -0
- data/lib/zip/inflater.rb +5 -0
- data/lib/zip/input_stream.rb +5 -0
- data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
- data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
- data/lib/zip/ioextras/fake_io.rb +5 -0
- data/lib/zip/ioextras.rb +5 -0
- data/lib/zip/null_compressor.rb +5 -0
- data/lib/zip/null_decompressor.rb +5 -0
- data/lib/zip/null_input_stream.rb +5 -0
- data/lib/zip/output_stream.rb +5 -0
- data/lib/zip/pass_thru_compressor.rb +5 -0
- data/lib/zip/pass_thru_decompressor.rb +5 -0
- data/lib/zip/streamable_directory.rb +5 -0
- data/lib/zip/streamable_stream.rb +5 -0
- data/lib/zip/version.rb +5 -0
- data/lib/zip.rb +19 -0
- data/lib/ziperu/3.1/ziperu.so +0 -0
- data/lib/ziperu/3.2/ziperu.so +0 -0
- data/lib/ziperu/3.3/ziperu.so +0 -0
- data/lib/ziperu/3.4/ziperu.so +0 -0
- data/lib/ziperu/4.0/ziperu.so +0 -0
- data/lib/ziperu/central_directory.rb +136 -0
- data/lib/ziperu/compressor.rb +14 -0
- data/lib/ziperu/constants.rb +148 -0
- data/lib/ziperu/crypto.rb +72 -0
- data/lib/ziperu/decompressor.rb +35 -0
- data/lib/ziperu/deflater.rb +41 -0
- data/lib/ziperu/dirtyable.rb +48 -0
- data/lib/ziperu/dos_time.rb +87 -0
- data/lib/ziperu/entry.rb +760 -0
- data/lib/ziperu/entry_set.rb +147 -0
- data/lib/ziperu/errors.rb +154 -0
- data/lib/ziperu/extra_field/aes.rb +52 -0
- data/lib/ziperu/extra_field/generic.rb +45 -0
- data/lib/ziperu/extra_field/ntfs.rb +98 -0
- data/lib/ziperu/extra_field/old_unix.rb +52 -0
- data/lib/ziperu/extra_field/universal_time.rb +81 -0
- data/lib/ziperu/extra_field/unix.rb +45 -0
- data/lib/ziperu/extra_field/unknown.rb +37 -0
- data/lib/ziperu/extra_field/zip64.rb +86 -0
- data/lib/ziperu/extra_field.rb +109 -0
- data/lib/ziperu/file.rb +614 -0
- data/lib/ziperu/file_split.rb +93 -0
- data/lib/ziperu/filesystem/dir.rb +88 -0
- data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
- data/lib/ziperu/filesystem/file.rb +265 -0
- data/lib/ziperu/filesystem/file_stat.rb +112 -0
- data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
- data/lib/ziperu/filesystem.rb +76 -0
- data/lib/ziperu/inflater.rb +60 -0
- data/lib/ziperu/input_stream.rb +296 -0
- data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
- data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
- data/lib/ziperu/ioextras/fake_io.rb +41 -0
- data/lib/ziperu/ioextras.rb +42 -0
- data/lib/ziperu/null_compressor.rb +20 -0
- data/lib/ziperu/null_decompressor.rb +25 -0
- data/lib/ziperu/null_input_stream.rb +55 -0
- data/lib/ziperu/output_stream.rb +223 -0
- data/lib/ziperu/pass_thru_compressor.rb +29 -0
- data/lib/ziperu/pass_thru_decompressor.rb +37 -0
- data/lib/ziperu/streamable_directory.rb +15 -0
- data/lib/ziperu/streamable_stream.rb +59 -0
- data/lib/ziperu/version.rb +7 -0
- data/lib/ziperu.rb +116 -0
- metadata +156 -0
data/lib/ziperu/entry.rb
ADDED
|
@@ -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
|