rubyzip 2.0.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -9
- data/Rakefile +3 -0
- data/lib/zip/central_directory.rb +9 -5
- data/lib/zip/constants.rb +52 -0
- data/lib/zip/crypto/decrypted_io.rb +40 -0
- data/lib/zip/crypto/traditional_encryption.rb +9 -9
- data/lib/zip/decompressor.rb +19 -1
- data/lib/zip/dos_time.rb +24 -12
- data/lib/zip/entry.rb +107 -49
- data/lib/zip/entry_set.rb +2 -0
- data/lib/zip/errors.rb +1 -0
- data/lib/zip/extra_field/generic.rb +10 -9
- data/lib/zip/extra_field/ntfs.rb +5 -1
- data/lib/zip/extra_field/old_unix.rb +3 -1
- data/lib/zip/extra_field/universal_time.rb +42 -12
- data/lib/zip/extra_field/unix.rb +3 -1
- data/lib/zip/extra_field/zip64.rb +5 -3
- data/lib/zip/extra_field.rb +11 -9
- data/lib/zip/file.rb +142 -65
- data/lib/zip/filesystem.rb +193 -177
- data/lib/zip/inflater.rb +24 -36
- data/lib/zip/input_stream.rb +50 -30
- data/lib/zip/ioextras/abstract_input_stream.rb +23 -12
- data/lib/zip/ioextras/abstract_output_stream.rb +1 -1
- data/lib/zip/ioextras.rb +3 -3
- data/lib/zip/null_decompressor.rb +1 -9
- data/lib/zip/output_stream.rb +28 -12
- data/lib/zip/pass_thru_compressor.rb +2 -2
- data/lib/zip/pass_thru_decompressor.rb +13 -22
- data/lib/zip/streamable_directory.rb +3 -3
- data/lib/zip/streamable_stream.rb +6 -10
- data/lib/zip/version.rb +1 -1
- data/lib/zip.rb +22 -2
- data/samples/example.rb +2 -2
- data/samples/example_filesystem.rb +1 -1
- data/samples/gtk_ruby_zip.rb +19 -19
- data/samples/qtzip.rb +6 -6
- data/samples/write_simple.rb +2 -4
- data/samples/zipfind.rb +23 -22
- metadata +52 -31
data/lib/zip/entry.rb
CHANGED
@@ -34,7 +34,7 @@ module Zip
|
|
34
34
|
end
|
35
35
|
@follow_symlinks = false
|
36
36
|
|
37
|
-
@restore_times =
|
37
|
+
@restore_times = false
|
38
38
|
@restore_permissions = false
|
39
39
|
@restore_ownership = false
|
40
40
|
# BUG: need an extra field to support uid/gid's
|
@@ -48,28 +48,52 @@ module Zip
|
|
48
48
|
|
49
49
|
def check_name(name)
|
50
50
|
return unless name.start_with?('/')
|
51
|
+
|
51
52
|
raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
|
52
53
|
end
|
53
54
|
|
54
|
-
|
55
|
-
|
55
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
56
|
+
def initialize(zipfile = nil, name = nil, *args)
|
57
|
+
name ||= ''
|
56
58
|
check_name(name)
|
57
59
|
|
58
60
|
set_default_vars_values
|
59
61
|
@fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
|
60
62
|
|
61
|
-
@zipfile =
|
63
|
+
@zipfile = zipfile || ''
|
62
64
|
@name = name
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
|
66
|
+
if (args_hash = args.first).kind_of?(::Hash)
|
67
|
+
@comment = args_hash[:comment] || ''
|
68
|
+
@extra = args_hash[:extra] || ''
|
69
|
+
@compressed_size = args_hash[:compressed_size] || 0
|
70
|
+
@crc = args_hash[:crc] || 0
|
71
|
+
@compression_method = args_hash[:compression_method] || ::Zip::Entry::DEFLATED
|
72
|
+
@size = args_hash[:size] || 0
|
73
|
+
@time = args_hash[:time] || ::Zip::DOSTime.now
|
74
|
+
else
|
75
|
+
Zip.warn_about_v3_api('Zip::Entry.new') unless args.empty?
|
76
|
+
|
77
|
+
@comment = args[0] || ''
|
78
|
+
@extra = args[1] || ''
|
79
|
+
@compressed_size = args[2] || 0
|
80
|
+
@crc = args[3] || 0
|
81
|
+
@compression_method = args[4] || ::Zip::Entry::DEFLATED
|
82
|
+
@size = args[5] || 0
|
83
|
+
@time = args[6] || ::Zip::DOSTime.now
|
84
|
+
end
|
70
85
|
|
71
86
|
@ftype = name_is_directory? ? :directory : :file
|
72
|
-
@extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.
|
87
|
+
@extra = ::Zip::ExtraField.new(@extra.to_s) unless @extra.kind_of?(::Zip::ExtraField)
|
88
|
+
end
|
89
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
90
|
+
|
91
|
+
def encrypted?
|
92
|
+
gp_flags & 1 == 1
|
93
|
+
end
|
94
|
+
|
95
|
+
def incomplete?
|
96
|
+
gp_flags & 8 == 8
|
73
97
|
end
|
74
98
|
|
75
99
|
def time
|
@@ -91,11 +115,12 @@ module Zip
|
|
91
115
|
@extra.create('UniversalTime')
|
92
116
|
end
|
93
117
|
(@extra['UniversalTime'] || @extra['NTFS']).mtime = value
|
94
|
-
@time
|
118
|
+
@time = value
|
95
119
|
end
|
96
120
|
|
97
121
|
def file_type_is?(type)
|
98
122
|
raise InternalError, "current filetype is unknown: #{inspect}" unless @ftype
|
123
|
+
|
99
124
|
@ftype == type
|
100
125
|
end
|
101
126
|
|
@@ -116,6 +141,7 @@ module Zip
|
|
116
141
|
def name_safe?
|
117
142
|
cleanpath = Pathname.new(@name).cleanpath
|
118
143
|
return false unless cleanpath.relative?
|
144
|
+
|
119
145
|
root = ::File::SEPARATOR
|
120
146
|
naive_expanded_path = ::File.join(root, cleanpath.to_s)
|
121
147
|
::File.absolute_path(cleanpath.to_s, root) == naive_expanded_path
|
@@ -145,6 +171,7 @@ module Zip
|
|
145
171
|
# that we didn't change the header size (and thus clobber file data or something)
|
146
172
|
def verify_local_header_size!
|
147
173
|
return if @local_header_size.nil?
|
174
|
+
|
148
175
|
new_size = calculate_local_header_size
|
149
176
|
raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size
|
150
177
|
end
|
@@ -162,20 +189,41 @@ module Zip
|
|
162
189
|
# NB: The caller is responsible for making sure dest_path is safe, if it
|
163
190
|
# is passed.
|
164
191
|
def extract(dest_path = nil, &block)
|
192
|
+
Zip.warn_about_v3_api('Zip::Entry#extract')
|
193
|
+
|
165
194
|
if dest_path.nil? && !name_safe?
|
166
|
-
|
195
|
+
warn "WARNING: skipped '#{@name}' as unsafe."
|
167
196
|
return self
|
168
197
|
end
|
169
198
|
|
170
199
|
dest_path ||= @name
|
171
200
|
block ||= proc { ::Zip.on_exists_proc }
|
172
201
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
202
|
+
raise "unknown file type #{inspect}" unless directory? || file? || symlink?
|
203
|
+
|
204
|
+
__send__("create_#{@ftype}", dest_path, &block)
|
205
|
+
self
|
206
|
+
end
|
207
|
+
|
208
|
+
# Extracts this entry to a file at `entry_path`, with
|
209
|
+
# `destination_directory` as the base location in the filesystem.
|
210
|
+
#
|
211
|
+
# NB: The caller is responsible for making sure `destination_directory` is
|
212
|
+
# safe, if it is passed.
|
213
|
+
def extract_v3(entry_path = @name, destination_directory: '.', &block)
|
214
|
+
dest_dir = ::File.absolute_path(destination_directory || '.')
|
215
|
+
extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
|
216
|
+
|
217
|
+
unless extract_path.start_with?(dest_dir)
|
218
|
+
warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
|
219
|
+
return self
|
177
220
|
end
|
178
221
|
|
222
|
+
block ||= proc { ::Zip.on_exists_proc }
|
223
|
+
|
224
|
+
raise "unknown file type #{inspect}" unless directory? || file? || symlink?
|
225
|
+
|
226
|
+
__send__(:"create_#{ftype}", extract_path, &block)
|
179
227
|
self
|
180
228
|
end
|
181
229
|
|
@@ -185,15 +233,15 @@ module Zip
|
|
185
233
|
|
186
234
|
class << self
|
187
235
|
def read_zip_short(io) # :nodoc:
|
188
|
-
io.read(2).
|
236
|
+
io.read(2).unpack1('v')
|
189
237
|
end
|
190
238
|
|
191
239
|
def read_zip_long(io) # :nodoc:
|
192
|
-
io.read(4).
|
240
|
+
io.read(4).unpack1('V')
|
193
241
|
end
|
194
242
|
|
195
243
|
def read_zip_64_long(io) # :nodoc:
|
196
|
-
io.read(8).
|
244
|
+
io.read(8).unpack1('Q<')
|
197
245
|
end
|
198
246
|
|
199
247
|
def read_c_dir_entry(io) #:nodoc:all
|
@@ -218,8 +266,6 @@ module Zip
|
|
218
266
|
end
|
219
267
|
end
|
220
268
|
|
221
|
-
public
|
222
|
-
|
223
269
|
def unpack_local_entry(buf)
|
224
270
|
@header_signature,
|
225
271
|
@version,
|
@@ -249,6 +295,7 @@ module Zip
|
|
249
295
|
unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE
|
250
296
|
raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'"
|
251
297
|
end
|
298
|
+
|
252
299
|
set_time(@last_mod_date, @last_mod_time)
|
253
300
|
|
254
301
|
@name = io.read(@name_length)
|
@@ -261,13 +308,14 @@ module Zip
|
|
261
308
|
|
262
309
|
if extra && extra.bytesize != @extra_length
|
263
310
|
raise ::Zip::Error, 'Truncated local zip entry header'
|
311
|
+
end
|
312
|
+
|
313
|
+
if @extra.kind_of?(::Zip::ExtraField)
|
314
|
+
@extra.merge(extra) if extra
|
264
315
|
else
|
265
|
-
|
266
|
-
@extra.merge(extra) if extra
|
267
|
-
else
|
268
|
-
@extra = ::Zip::ExtraField.new(extra)
|
269
|
-
end
|
316
|
+
@extra = ::Zip::ExtraField.new(extra)
|
270
317
|
end
|
318
|
+
|
271
319
|
parse_zip64_extra(true)
|
272
320
|
@local_header_size = calculate_local_header_size
|
273
321
|
end
|
@@ -354,21 +402,24 @@ module Zip
|
|
354
402
|
|
355
403
|
def check_c_dir_entry_static_header_length(buf)
|
356
404
|
return if buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
|
405
|
+
|
357
406
|
raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
|
358
407
|
end
|
359
408
|
|
360
409
|
def check_c_dir_entry_signature
|
361
410
|
return if header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
|
411
|
+
|
362
412
|
raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
|
363
413
|
end
|
364
414
|
|
365
415
|
def check_c_dir_entry_comment_size
|
366
416
|
return if @comment && @comment.bytesize == @comment_length
|
417
|
+
|
367
418
|
raise ::Zip::Error, 'Truncated cdir zip entry header'
|
368
419
|
end
|
369
420
|
|
370
421
|
def read_c_dir_extra_field(io)
|
371
|
-
if @extra.
|
422
|
+
if @extra.kind_of?(::Zip::ExtraField)
|
372
423
|
@extra.merge(io.read(@extra_length))
|
373
424
|
else
|
374
425
|
@extra = ::Zip::ExtraField.new(io.read(@extra_length))
|
@@ -402,20 +453,25 @@ module Zip
|
|
402
453
|
|
403
454
|
def get_extra_attributes_from_path(path) # :nodoc:
|
404
455
|
return if Zip::RUNNING_ON_WINDOWS
|
456
|
+
|
405
457
|
stat = file_stat(path)
|
406
458
|
@unix_uid = stat.uid
|
407
459
|
@unix_gid = stat.gid
|
408
460
|
@unix_perms = stat.mode & 0o7777
|
461
|
+
@time = ::Zip::DOSTime.from_time(stat.mtime)
|
409
462
|
end
|
410
463
|
|
411
|
-
def
|
412
|
-
# BUG: does not update timestamps into account
|
464
|
+
def set_unix_attributes_on_path(dest_path)
|
413
465
|
# ignore setuid/setgid bits by default. honor if @restore_ownership
|
414
466
|
unix_perms_mask = 0o1777
|
415
467
|
unix_perms_mask = 0o7777 if @restore_ownership
|
416
468
|
::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
|
417
469
|
::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
|
418
|
-
|
470
|
+
|
471
|
+
# Restore the timestamp on a file. This will either have come from the
|
472
|
+
# original source file that was copied into the archive, or from the
|
473
|
+
# creation date of the archive if there was no original source file.
|
474
|
+
::FileUtils.touch(dest_path, mtime: time) if @restore_times
|
419
475
|
end
|
420
476
|
|
421
477
|
def set_extra_attributes_on_path(dest_path) # :nodoc:
|
@@ -423,7 +479,7 @@ module Zip
|
|
423
479
|
|
424
480
|
case @fstype
|
425
481
|
when ::Zip::FSTYPE_UNIX
|
426
|
-
|
482
|
+
set_unix_attributes_on_path(dest_path)
|
427
483
|
end
|
428
484
|
end
|
429
485
|
|
@@ -484,11 +540,12 @@ module Zip
|
|
484
540
|
|
485
541
|
def ==(other)
|
486
542
|
return false unless other.class == self.class
|
543
|
+
|
487
544
|
# Compares contents of local entry and exposed fields
|
488
545
|
keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
|
489
546
|
other.__send__(k.to_sym) == __send__(k.to_sym)
|
490
547
|
end
|
491
|
-
keys_equal && time
|
548
|
+
keys_equal && time == other.time
|
492
549
|
end
|
493
550
|
|
494
551
|
def <=>(other)
|
@@ -514,7 +571,7 @@ module Zip
|
|
514
571
|
raise "unknown @file_type #{@ftype}"
|
515
572
|
end
|
516
573
|
else
|
517
|
-
zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
|
574
|
+
zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
|
518
575
|
zis.instance_variable_set(:@complete_entry, self)
|
519
576
|
zis.get_next_entry
|
520
577
|
if block_given?
|
@@ -591,7 +648,7 @@ module Zip
|
|
591
648
|
def set_time(binary_dos_date, binary_dos_time)
|
592
649
|
@time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
|
593
650
|
rescue ArgumentError
|
594
|
-
warn '
|
651
|
+
warn 'WARNING: invalid date/time in zip entry.' if ::Zip.warn_invalid_date
|
595
652
|
end
|
596
653
|
|
597
654
|
def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
|
@@ -601,30 +658,29 @@ module Zip
|
|
601
658
|
end
|
602
659
|
::File.open(dest_path, 'wb') do |os|
|
603
660
|
get_input_stream do |is|
|
604
|
-
set_extra_attributes_on_path(dest_path)
|
605
|
-
|
606
661
|
bytes_written = 0
|
607
662
|
warned = false
|
608
|
-
buf = ''
|
663
|
+
buf = +''
|
609
664
|
while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
|
610
665
|
os << buf
|
611
666
|
bytes_written += buf.bytesize
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
end
|
620
|
-
end
|
667
|
+
next unless bytes_written > size && !warned
|
668
|
+
|
669
|
+
message = "entry '#{name}' should be #{size}B, but is larger when inflated."
|
670
|
+
raise ::Zip::EntrySizeError, message if ::Zip.validate_entry_sizes
|
671
|
+
|
672
|
+
warn "WARNING: #{message}"
|
673
|
+
warned = true
|
621
674
|
end
|
622
675
|
end
|
623
676
|
end
|
677
|
+
|
678
|
+
set_extra_attributes_on_path(dest_path)
|
624
679
|
end
|
625
680
|
|
626
681
|
def create_directory(dest_path)
|
627
682
|
return if ::File.directory?(dest_path)
|
683
|
+
|
628
684
|
if ::File.exist?(dest_path)
|
629
685
|
if block_given? && yield(self, dest_path)
|
630
686
|
::FileUtils.rm_f dest_path
|
@@ -642,13 +698,14 @@ module Zip
|
|
642
698
|
def create_symlink(dest_path)
|
643
699
|
# TODO: Symlinks pose security challenges. Symlink support temporarily
|
644
700
|
# removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
|
645
|
-
|
701
|
+
warn "WARNING: skipped symlink '#{dest_path}'."
|
646
702
|
end
|
647
703
|
|
648
704
|
# apply missing data from the zip64 extra information field, if present
|
649
705
|
# (required when file sizes exceed 2**32, but can be used for all files)
|
650
706
|
def parse_zip64_extra(for_local_header) #:nodoc:all
|
651
707
|
return if @extra['Zip64'].nil?
|
708
|
+
|
652
709
|
if for_local_header
|
653
710
|
@size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
|
654
711
|
else
|
@@ -663,6 +720,7 @@ module Zip
|
|
663
720
|
# create a zip64 extra information field if we need one
|
664
721
|
def prep_zip64_extra(for_local_header) #:nodoc:all
|
665
722
|
return unless ::Zip.write_zip64_support
|
723
|
+
|
666
724
|
need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
|
667
725
|
need_zip64 ||= @local_header_offset >= 0xFFFFFFFF unless for_local_header
|
668
726
|
if need_zip64
|
data/lib/zip/entry_set.rb
CHANGED
@@ -50,6 +50,7 @@ module Zip
|
|
50
50
|
|
51
51
|
def ==(other)
|
52
52
|
return false unless other.kind_of?(EntrySet)
|
53
|
+
|
53
54
|
@entry_set.values == other.entry_set.values
|
54
55
|
end
|
55
56
|
|
@@ -60,6 +61,7 @@ module Zip
|
|
60
61
|
def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
|
61
62
|
entries.map do |entry|
|
62
63
|
next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
|
64
|
+
|
63
65
|
yield(entry) if block_given?
|
64
66
|
entry
|
65
67
|
end.compact
|
data/lib/zip/errors.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Zip
|
2
2
|
class ExtraField::Generic
|
3
3
|
def self.register_map
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
return unless const_defined?(:HEADER_ID)
|
5
|
+
|
6
|
+
::Zip::ExtraField::ID_MAP[const_get(:HEADER_ID)] = self
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.name
|
@@ -12,18 +12,19 @@ module Zip
|
|
12
12
|
|
13
13
|
# return field [size, content] or false
|
14
14
|
def initial_parse(binstr)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
$stderr.puts 'Warning: weired extra feild header ID. skip parsing'
|
15
|
+
return false unless binstr
|
16
|
+
|
17
|
+
if binstr[0, 2] != self.class.const_get(:HEADER_ID)
|
18
|
+
warn 'WARNING: weird extra field header ID. Skip parsing it.'
|
20
19
|
return false
|
21
20
|
end
|
22
|
-
|
21
|
+
|
22
|
+
[binstr[2, 2].unpack1('v'), binstr[4..-1]]
|
23
23
|
end
|
24
24
|
|
25
25
|
def ==(other)
|
26
26
|
return false if self.class != other.class
|
27
|
+
|
27
28
|
each do |k, v|
|
28
29
|
return false if v != other[k]
|
29
30
|
end
|
data/lib/zip/extra_field/ntfs.rb
CHANGED
@@ -19,6 +19,7 @@ module Zip
|
|
19
19
|
|
20
20
|
def merge(binstr)
|
21
21
|
return if binstr.empty?
|
22
|
+
|
22
23
|
size, content = initial_parse(binstr)
|
23
24
|
(size && content) || return
|
24
25
|
|
@@ -27,6 +28,7 @@ module Zip
|
|
27
28
|
|
28
29
|
tag1 = tags[1]
|
29
30
|
return unless tag1
|
31
|
+
|
30
32
|
ntfs_mtime, ntfs_atime, ntfs_ctime = tag1.unpack('Q<Q<Q<')
|
31
33
|
ntfs_mtime && @mtime ||= from_ntfs_time(ntfs_mtime)
|
32
34
|
ntfs_atime && @atime ||= from_ntfs_time(ntfs_atime)
|
@@ -49,7 +51,7 @@ module Zip
|
|
49
51
|
# reserved 0 and tag 1
|
50
52
|
s = [0, 1].pack('Vv')
|
51
53
|
|
52
|
-
tag1 = ''.
|
54
|
+
tag1 = ''.b
|
53
55
|
if @mtime
|
54
56
|
tag1 << [to_ntfs_time(@mtime)].pack('Q<')
|
55
57
|
if @atime
|
@@ -65,12 +67,14 @@ module Zip
|
|
65
67
|
|
66
68
|
def parse_tags(content)
|
67
69
|
return {} if content.nil?
|
70
|
+
|
68
71
|
tags = {}
|
69
72
|
i = 0
|
70
73
|
while i < content.bytesize
|
71
74
|
tag, size = content[i, 4].unpack('vv')
|
72
75
|
i += 4
|
73
76
|
break unless tag && size
|
77
|
+
|
74
78
|
value = content[i, size]
|
75
79
|
i += size
|
76
80
|
tags[tag] = value
|
@@ -16,14 +16,16 @@ module Zip
|
|
16
16
|
|
17
17
|
def merge(binstr)
|
18
18
|
return if binstr.empty?
|
19
|
+
|
19
20
|
size, content = initial_parse(binstr)
|
20
21
|
# size: 0 for central directory. 4 for local header
|
21
22
|
return if !size || size == 0
|
23
|
+
|
22
24
|
atime, mtime, uid, gid = content.unpack('VVvv')
|
23
25
|
@uid ||= uid
|
24
26
|
@gid ||= gid
|
25
27
|
@atime ||= atime
|
26
|
-
@mtime ||= mtime
|
28
|
+
@mtime ||= mtime # rubocop:disable Naming/MemoizedInstanceVariableName
|
27
29
|
end
|
28
30
|
|
29
31
|
def ==(other)
|
@@ -4,24 +4,54 @@ module Zip
|
|
4
4
|
HEADER_ID = 'UT'
|
5
5
|
register_map
|
6
6
|
|
7
|
+
ATIME_MASK = 0b010
|
8
|
+
CTIME_MASK = 0b100
|
9
|
+
MTIME_MASK = 0b001
|
10
|
+
|
7
11
|
def initialize(binstr = nil)
|
8
12
|
@ctime = nil
|
9
13
|
@mtime = nil
|
10
14
|
@atime = nil
|
11
|
-
@flag =
|
12
|
-
|
15
|
+
@flag = 0
|
16
|
+
|
17
|
+
merge(binstr) unless binstr.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :atime, :ctime, :mtime, :flag
|
21
|
+
|
22
|
+
def atime=(time)
|
23
|
+
@flag = time.nil? ? @flag & ~ATIME_MASK : @flag | ATIME_MASK
|
24
|
+
@atime = time
|
25
|
+
end
|
26
|
+
|
27
|
+
def ctime=(time)
|
28
|
+
@flag = time.nil? ? @flag & ~CTIME_MASK : @flag | CTIME_MASK
|
29
|
+
@ctime = time
|
13
30
|
end
|
14
31
|
|
15
|
-
|
32
|
+
def mtime=(time)
|
33
|
+
@flag = time.nil? ? @flag & ~MTIME_MASK : @flag | MTIME_MASK
|
34
|
+
@mtime = time
|
35
|
+
end
|
16
36
|
|
17
37
|
def merge(binstr)
|
18
38
|
return if binstr.empty?
|
39
|
+
|
19
40
|
size, content = initial_parse(binstr)
|
20
|
-
size ||
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
41
|
+
return if !size || size <= 0
|
42
|
+
|
43
|
+
@flag, *times = content.unpack('Cl<l<l<')
|
44
|
+
|
45
|
+
# Parse the timestamps, in order, based on which flags are set.
|
46
|
+
return if times[0].nil?
|
47
|
+
|
48
|
+
@mtime ||= ::Zip::DOSTime.at(times.shift) unless @flag & MTIME_MASK == 0
|
49
|
+
return if times[0].nil?
|
50
|
+
|
51
|
+
@atime ||= ::Zip::DOSTime.at(times.shift) unless @flag & ATIME_MASK == 0
|
52
|
+
return if times[0].nil?
|
53
|
+
|
54
|
+
@ctime ||= ::Zip::DOSTime.at(times.shift) unless @flag & CTIME_MASK == 0
|
25
55
|
end
|
26
56
|
|
27
57
|
def ==(other)
|
@@ -32,15 +62,15 @@ module Zip
|
|
32
62
|
|
33
63
|
def pack_for_local
|
34
64
|
s = [@flag].pack('C')
|
35
|
-
|
36
|
-
|
37
|
-
|
65
|
+
s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
|
66
|
+
s << [@atime.to_i].pack('l<') unless @flag & ATIME_MASK == 0
|
67
|
+
s << [@ctime.to_i].pack('l<') unless @flag & CTIME_MASK == 0
|
38
68
|
s
|
39
69
|
end
|
40
70
|
|
41
71
|
def pack_for_c_dir
|
42
72
|
s = [@flag].pack('C')
|
43
|
-
|
73
|
+
s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
|
44
74
|
s
|
45
75
|
end
|
46
76
|
end
|
data/lib/zip/extra_field/unix.rb
CHANGED
@@ -14,12 +14,14 @@ module Zip
|
|
14
14
|
|
15
15
|
def merge(binstr)
|
16
16
|
return if binstr.empty?
|
17
|
+
|
17
18
|
size, content = initial_parse(binstr)
|
18
19
|
# size: 0 for central directory. 4 for local header
|
19
20
|
return if !size || size == 0
|
21
|
+
|
20
22
|
uid, gid = content.unpack('vv')
|
21
23
|
@uid ||= uid
|
22
|
-
@gid ||= gid
|
24
|
+
@gid ||= gid # rubocop:disable Naming/MemoizedInstanceVariableName
|
23
25
|
end
|
24
26
|
|
25
27
|
def ==(other)
|
@@ -9,7 +9,7 @@ module Zip
|
|
9
9
|
# unparsed binary; we don't actually know what this contains
|
10
10
|
# without looking for FFs in the associated file header
|
11
11
|
# call parse after initializing with a binary string
|
12
|
-
@content
|
12
|
+
@content = nil
|
13
13
|
@original_size = nil
|
14
14
|
@compressed_size = nil
|
15
15
|
@relative_header_offset = nil
|
@@ -26,6 +26,7 @@ module Zip
|
|
26
26
|
|
27
27
|
def merge(binstr)
|
28
28
|
return if binstr.empty?
|
29
|
+
|
29
30
|
_, @content = initial_parse(binstr)
|
30
31
|
end
|
31
32
|
|
@@ -45,19 +46,20 @@ module Zip
|
|
45
46
|
end
|
46
47
|
|
47
48
|
def extract(size, format)
|
48
|
-
@content.slice!(0, size).
|
49
|
+
@content.slice!(0, size).unpack1(format)
|
49
50
|
end
|
50
51
|
private :extract
|
51
52
|
|
52
53
|
def pack_for_local
|
53
54
|
# local header entries must contain original size and compressed size; other fields do not apply
|
54
55
|
return '' unless @original_size && @compressed_size
|
56
|
+
|
55
57
|
[@original_size, @compressed_size].pack('Q<Q<')
|
56
58
|
end
|
57
59
|
|
58
60
|
def pack_for_c_dir
|
59
61
|
# central directory entries contain only fields that didn't fit in the main entry part
|
60
|
-
packed = ''.
|
62
|
+
packed = ''.b
|
61
63
|
packed << [@original_size].pack('Q<') if @original_size
|
62
64
|
packed << [@compressed_size].pack('Q<') if @compressed_size
|
63
65
|
packed << [@relative_header_offset].pack('Q<') if @relative_header_offset
|
data/lib/zip/extra_field.rb
CHANGED
@@ -6,27 +6,27 @@ module Zip
|
|
6
6
|
merge(binstr) if binstr
|
7
7
|
end
|
8
8
|
|
9
|
-
def extra_field_type_exist(binstr, id, len,
|
9
|
+
def extra_field_type_exist(binstr, id, len, index)
|
10
10
|
field_name = ID_MAP[id].name
|
11
11
|
if member?(field_name)
|
12
|
-
self[field_name].merge(binstr[
|
12
|
+
self[field_name].merge(binstr[index, len + 4])
|
13
13
|
else
|
14
|
-
field_obj = ID_MAP[id].new(binstr[
|
14
|
+
field_obj = ID_MAP[id].new(binstr[index, len + 4])
|
15
15
|
self[field_name] = field_obj
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
def extra_field_type_unknown(binstr, len,
|
19
|
+
def extra_field_type_unknown(binstr, len, index)
|
20
20
|
create_unknown_item unless self['Unknown']
|
21
|
-
if !len || len + 4 > binstr[
|
22
|
-
self['Unknown'] << binstr[
|
21
|
+
if !len || len + 4 > binstr[index..-1].bytesize
|
22
|
+
self['Unknown'] << binstr[index..-1]
|
23
23
|
return
|
24
24
|
end
|
25
|
-
self['Unknown'] << binstr[
|
25
|
+
self['Unknown'] << binstr[index, len + 4]
|
26
26
|
end
|
27
27
|
|
28
28
|
def create_unknown_item
|
29
|
-
s = ''
|
29
|
+
s = +''
|
30
30
|
class << s
|
31
31
|
alias_method :to_c_dir_bin, :to_s
|
32
32
|
alias_method :to_local_bin, :to_s
|
@@ -36,10 +36,11 @@ module Zip
|
|
36
36
|
|
37
37
|
def merge(binstr)
|
38
38
|
return if binstr.empty?
|
39
|
+
|
39
40
|
i = 0
|
40
41
|
while i < binstr.bytesize
|
41
42
|
id = binstr[i, 2]
|
42
|
-
len = binstr[i + 2, 2].to_s.
|
43
|
+
len = binstr[i + 2, 2].to_s.unpack1('v')
|
43
44
|
if id && ID_MAP.member?(id)
|
44
45
|
extra_field_type_exist(binstr, id, len, i)
|
45
46
|
elsif id
|
@@ -54,6 +55,7 @@ module Zip
|
|
54
55
|
unless (field_class = ID_MAP.values.find { |k| k.name == name })
|
55
56
|
raise Error, "Unknown extra field '#{name}'"
|
56
57
|
end
|
58
|
+
|
57
59
|
self[name] = field_class.new
|
58
60
|
end
|
59
61
|
|