rubyzip 1.1.6 → 1.1.7
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rubyzip might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +66 -13
- data/lib/zip.rb +5 -1
- data/lib/zip/central_directory.rb +1 -1
- data/lib/zip/crypto/encryption.rb +11 -0
- data/lib/zip/crypto/null_encryption.rb +45 -0
- data/lib/zip/crypto/traditional_encryption.rb +99 -0
- data/lib/zip/deflater.rb +6 -3
- data/lib/zip/entry.rb +11 -5
- data/lib/zip/entry_set.rb +5 -6
- data/lib/zip/extra_field.rb +1 -0
- data/lib/zip/extra_field/ntfs.rb +92 -0
- data/lib/zip/file.rb +11 -1
- data/lib/zip/filesystem.rb +4 -0
- data/lib/zip/inflater.rb +4 -3
- data/lib/zip/input_stream.rb +10 -4
- data/lib/zip/output_stream.rb +12 -7
- data/lib/zip/version.rb +1 -1
- data/test/basic_zip_file_test.rb +1 -1
- data/test/central_directory_entry_test.rb +1 -1
- data/test/central_directory_test.rb +5 -1
- data/test/crypto/null_encryption_test.rb +53 -0
- data/test/crypto/traditional_encryption_test.rb +80 -0
- data/test/data/WarnInvalidDate.zip +0 -0
- data/test/data/ntfs.zip +0 -0
- data/test/data/zipWithEncryption.zip +0 -0
- data/test/deflater_test.rb +14 -9
- data/test/encryption_test.rb +42 -0
- data/test/entry_set_test.rb +14 -1
- data/test/entry_test.rb +2 -2
- data/test/errors_test.rb +1 -1
- data/test/extra_field_test.rb +10 -1
- data/test/file_extract_directory_test.rb +3 -2
- data/test/file_extract_test.rb +2 -2
- data/test/file_split_test.rb +2 -2
- data/test/file_test.rb +16 -13
- data/test/filesystem/dir_iterator_test.rb +1 -1
- data/test/filesystem/directory_test.rb +1 -1
- data/test/filesystem/file_mutating_test.rb +1 -1
- data/test/filesystem/file_nonmutating_test.rb +10 -1
- data/test/filesystem/file_stat_test.rb +1 -1
- data/test/inflater_test.rb +1 -1
- data/test/input_stream_test.rb +1 -1
- data/test/ioextras/abstract_input_stream_test.rb +1 -1
- data/test/ioextras/abstract_output_stream_test.rb +1 -1
- data/test/ioextras/fake_io_test.rb +1 -1
- data/test/local_entry_test.rb +14 -11
- data/test/output_stream_test.rb +18 -3
- data/test/pass_thru_compressor_test.rb +2 -2
- data/test/pass_thru_decompressor_test.rb +1 -1
- data/test/settings_test.rb +23 -2
- data/test/test_helper.rb +1 -1
- data/test/unicode_file_names_and_comments_test.rb +16 -4
- data/test/zip64_full_test.rb +5 -1
- data/test/zip64_support_test.rb +1 -1
- metadata +104 -6
- data/test/dummy.txt +0 -1
@@ -0,0 +1,92 @@
|
|
1
|
+
module Zip
|
2
|
+
# PKWARE NTFS Extra Field (0x000a)
|
3
|
+
# Only Tag 0x0001 is supported
|
4
|
+
class ExtraField::NTFS < ExtraField::Generic
|
5
|
+
HEADER_ID = [0x000A].pack('v')
|
6
|
+
register_map
|
7
|
+
|
8
|
+
WINDOWS_TICK = 10000000.0
|
9
|
+
SEC_TO_UNIX_EPOCH = 11644473600
|
10
|
+
|
11
|
+
def initialize(binstr = nil)
|
12
|
+
@ctime = nil
|
13
|
+
@mtime = nil
|
14
|
+
@atime = nil
|
15
|
+
binstr and merge(binstr)
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :atime, :ctime, :mtime
|
19
|
+
|
20
|
+
def merge(binstr)
|
21
|
+
return if binstr.empty?
|
22
|
+
size, content = initial_parse(binstr)
|
23
|
+
(size && content) or return
|
24
|
+
|
25
|
+
content = content[4..-1]
|
26
|
+
tags = parse_tags(content)
|
27
|
+
|
28
|
+
tag1 = tags[1]
|
29
|
+
if tag1
|
30
|
+
ntfs_mtime, ntfs_atime, ntfs_ctime = tag1.unpack("Q<Q<Q<")
|
31
|
+
ntfs_mtime and @mtime ||= from_ntfs_time(ntfs_mtime)
|
32
|
+
ntfs_atime and @atime ||= from_ntfs_time(ntfs_atime)
|
33
|
+
ntfs_ctime and @ctime ||= from_ntfs_time(ntfs_ctime)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
@mtime == other.mtime &&
|
39
|
+
@atime == other.atime &&
|
40
|
+
@ctime == other.ctime
|
41
|
+
end
|
42
|
+
|
43
|
+
# Info-ZIP note states this extra field is stored at local header
|
44
|
+
def pack_for_local
|
45
|
+
pack_for_c_dir
|
46
|
+
end
|
47
|
+
|
48
|
+
# But 7-zip for Windows only stores at central dir
|
49
|
+
def pack_for_c_dir
|
50
|
+
# reserved 0 and tag 1
|
51
|
+
s = [0, 1].pack("Vv")
|
52
|
+
|
53
|
+
tag1 = ''.force_encoding(Encoding::BINARY)
|
54
|
+
if @mtime
|
55
|
+
tag1 << [to_ntfs_time(@mtime)].pack('Q<')
|
56
|
+
if @atime
|
57
|
+
tag1 << [to_ntfs_time(@atime)].pack('Q<')
|
58
|
+
if @ctime
|
59
|
+
tag1 << [to_ntfs_time(@ctime)].pack('Q<')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
s << [tag1.bytesize].pack('v') << tag1
|
64
|
+
s
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def parse_tags(content)
|
69
|
+
return {} if content.nil?
|
70
|
+
tags = {}
|
71
|
+
i = 0
|
72
|
+
while i < content.bytesize do
|
73
|
+
tag, size = content[i, 4].unpack('vv')
|
74
|
+
i += 4
|
75
|
+
break unless tag && size
|
76
|
+
value = content[i, size]
|
77
|
+
i += size
|
78
|
+
tags[tag] = value
|
79
|
+
end
|
80
|
+
|
81
|
+
tags
|
82
|
+
end
|
83
|
+
|
84
|
+
def from_ntfs_time(ntfs_time)
|
85
|
+
::Zip::DOSTime.at(ntfs_time / WINDOWS_TICK - SEC_TO_UNIX_EPOCH)
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_ntfs_time(time)
|
89
|
+
((time.to_f + SEC_TO_UNIX_EPOCH) * WINDOWS_TICK).to_i
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/zip/file.rb
CHANGED
@@ -121,11 +121,18 @@ module Zip
|
|
121
121
|
if io.is_a?(::String)
|
122
122
|
require 'stringio'
|
123
123
|
io = ::StringIO.new(io)
|
124
|
+
elsif io.is_a?(IO)
|
125
|
+
# https://github.com/rubyzip/rubyzip/issues/119
|
126
|
+
io.binmode
|
124
127
|
end
|
125
128
|
zf = ::Zip::File.new(io, true, true, options)
|
126
129
|
zf.read_from_stream(io)
|
127
130
|
yield zf
|
128
|
-
|
131
|
+
begin
|
132
|
+
zf.write_buffer(io)
|
133
|
+
rescue IOError => e
|
134
|
+
raise unless e.message == "not opened for writing"
|
135
|
+
end
|
129
136
|
end
|
130
137
|
|
131
138
|
# Iterates over the contents of the ZipFile. This is more efficient
|
@@ -403,6 +410,7 @@ module Zip
|
|
403
410
|
def on_success_replace
|
404
411
|
tmpfile = get_tempfile
|
405
412
|
tmp_filename = tmpfile.path
|
413
|
+
ObjectSpace.undefine_finalizer(tmpfile)
|
406
414
|
tmpfile.close
|
407
415
|
if yield tmp_filename
|
408
416
|
::File.rename(tmp_filename, self.name)
|
@@ -410,6 +418,8 @@ module Zip
|
|
410
418
|
::File.chmod(@exist_file_perms, self.name)
|
411
419
|
end
|
412
420
|
end
|
421
|
+
ensure
|
422
|
+
tmpfile.unlink if tmpfile
|
413
423
|
end
|
414
424
|
|
415
425
|
def get_tempfile
|
data/lib/zip/filesystem.rb
CHANGED
@@ -320,6 +320,8 @@ module Zip
|
|
320
320
|
e = get_entry(fileName)
|
321
321
|
if e.extra.member? "UniversalTime"
|
322
322
|
e.extra["UniversalTime"].atime
|
323
|
+
elsif e.extra.member? "NTFS"
|
324
|
+
e.extra["NTFS"].atime
|
323
325
|
else
|
324
326
|
nil
|
325
327
|
end
|
@@ -329,6 +331,8 @@ module Zip
|
|
329
331
|
e = get_entry(fileName)
|
330
332
|
if e.extra.member? "UniversalTime"
|
331
333
|
e.extra["UniversalTime"].ctime
|
334
|
+
elsif e.extra.member? "NTFS"
|
335
|
+
e.extra["NTFS"].ctime
|
332
336
|
else
|
333
337
|
nil
|
334
338
|
end
|
data/lib/zip/inflater.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
module Zip
|
2
2
|
class Inflater < Decompressor #:nodoc:all
|
3
|
-
def initialize(input_stream)
|
4
|
-
super
|
3
|
+
def initialize(input_stream, decrypter = NullDecrypter.new)
|
4
|
+
super(input_stream)
|
5
5
|
@zlib_inflater = ::Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
6
6
|
@output_buffer = ''
|
7
7
|
@has_returned_empty_string = false
|
8
|
+
@decrypter = decrypter
|
8
9
|
end
|
9
10
|
|
10
11
|
def sysread(number_of_bytes = nil, buf = '')
|
@@ -40,7 +41,7 @@ module Zip
|
|
40
41
|
def internal_produce_input(buf = '')
|
41
42
|
retried = 0
|
42
43
|
begin
|
43
|
-
@zlib_inflater.inflate(@input_stream.read(Decompressor::CHUNK_SIZE, buf))
|
44
|
+
@zlib_inflater.inflate(@decrypter.decrypt(@input_stream.read(Decompressor::CHUNK_SIZE, buf)))
|
44
45
|
rescue Zlib::BufError
|
45
46
|
raise if retried >= 5 # how many times should we retry?
|
46
47
|
retried += 1
|
data/lib/zip/input_stream.rb
CHANGED
@@ -47,10 +47,11 @@ module Zip
|
|
47
47
|
#
|
48
48
|
# @param context [String||IO||StringIO] file path or IO/StringIO object
|
49
49
|
# @param offset [Integer] offset in the IO/StringIO
|
50
|
-
def initialize(context, offset = 0)
|
50
|
+
def initialize(context, offset = 0, decrypter = nil)
|
51
51
|
super()
|
52
52
|
@archive_io = get_io(context, offset)
|
53
53
|
@decompressor = ::Zip::NullDecompressor
|
54
|
+
@decrypter = decrypter || ::Zip::NullDecrypter.new
|
54
55
|
@current_entry = nil
|
55
56
|
end
|
56
57
|
|
@@ -91,8 +92,8 @@ module Zip
|
|
91
92
|
# Same as #initialize but if a block is passed the opened
|
92
93
|
# stream is passed to the block and closed when the block
|
93
94
|
# returns.
|
94
|
-
def open(filename_or_io, offset = 0)
|
95
|
-
zio = self.new(filename_or_io, offset)
|
95
|
+
def open(filename_or_io, offset = 0, decrypter = nil)
|
96
|
+
zio = self.new(filename_or_io, offset, decrypter)
|
96
97
|
return zio unless block_given?
|
97
98
|
begin
|
98
99
|
yield zio
|
@@ -124,6 +125,9 @@ module Zip
|
|
124
125
|
|
125
126
|
def open_entry
|
126
127
|
@current_entry = ::Zip::Entry.read_local_entry(@archive_io)
|
128
|
+
if @current_entry and @current_entry.gp_flags & 1 == 1 and @decrypter.is_a? NullEncrypter
|
129
|
+
raise Error, 'password required to decode zip file'
|
130
|
+
end
|
127
131
|
@decompressor = get_decompressor
|
128
132
|
flush
|
129
133
|
@current_entry
|
@@ -136,7 +140,9 @@ module Zip
|
|
136
140
|
when @current_entry.compression_method == ::Zip::Entry::STORED
|
137
141
|
::Zip::PassThruDecompressor.new(@archive_io, @current_entry.size)
|
138
142
|
when @current_entry.compression_method == ::Zip::Entry::DEFLATED
|
139
|
-
|
143
|
+
header = @archive_io.read(@decrypter.header_bytesize)
|
144
|
+
@decrypter.reset!(header)
|
145
|
+
::Zip::Inflater.new(@archive_io, @decrypter)
|
140
146
|
else
|
141
147
|
raise ::Zip::CompressionMethodError,
|
142
148
|
"Unsupported compression method #{@current_entry.compression_method}"
|
data/lib/zip/output_stream.rb
CHANGED
@@ -24,12 +24,12 @@ module Zip
|
|
24
24
|
|
25
25
|
# Opens the indicated zip file. If a file with that name already
|
26
26
|
# exists it will be overwritten.
|
27
|
-
def initialize(file_name, stream=false)
|
27
|
+
def initialize(file_name, stream=false, encrypter=nil)
|
28
28
|
super()
|
29
29
|
@file_name = file_name
|
30
30
|
@output_stream = if stream
|
31
31
|
iostream = @file_name.dup
|
32
|
-
iostream.reopen
|
32
|
+
iostream.reopen(@file_name)
|
33
33
|
iostream.rewind
|
34
34
|
iostream
|
35
35
|
else
|
@@ -37,6 +37,7 @@ module Zip
|
|
37
37
|
end
|
38
38
|
@entry_set = ::Zip::EntrySet.new
|
39
39
|
@compressor = ::Zip::NullCompressor.instance
|
40
|
+
@encrypter = encrypter || ::Zip::NullEncrypter.new
|
40
41
|
@closed = false
|
41
42
|
@current_entry = nil
|
42
43
|
@comment = nil
|
@@ -46,17 +47,17 @@ module Zip
|
|
46
47
|
# stream is passed to the block and closed when the block
|
47
48
|
# returns.
|
48
49
|
class << self
|
49
|
-
def open(file_name)
|
50
|
+
def open(file_name, encrypter = nil)
|
50
51
|
return new(file_name) unless block_given?
|
51
|
-
zos = new(file_name)
|
52
|
+
zos = new(file_name, false, encrypter)
|
52
53
|
yield zos
|
53
54
|
ensure
|
54
55
|
zos.close if zos
|
55
56
|
end
|
56
57
|
|
57
58
|
# Same as #open but writes to a filestream instead
|
58
|
-
def write_buffer(io = ::StringIO.new(''))
|
59
|
-
zos = new(io, true)
|
59
|
+
def write_buffer(io = ::StringIO.new(''), encrypter = nil)
|
60
|
+
zos = new(io, true, encrypter)
|
60
61
|
yield zos
|
61
62
|
zos.close_buffer
|
62
63
|
end
|
@@ -122,10 +123,13 @@ module Zip
|
|
122
123
|
|
123
124
|
def finalize_current_entry
|
124
125
|
return unless @current_entry
|
126
|
+
@output_stream << @encrypter.header(@current_entry.mtime)
|
125
127
|
finish
|
126
128
|
@current_entry.compressed_size = @output_stream.tell - @current_entry.local_header_offset - @current_entry.calculate_local_header_size
|
127
129
|
@current_entry.size = @compressor.size
|
128
130
|
@current_entry.crc = @compressor.crc
|
131
|
+
@output_stream << @encrypter.data_descriptor(@current_entry.crc, @current_entry.compressed_size, @current_entry.size)
|
132
|
+
@current_entry.gp_flags |= @encrypter.gp_flags
|
129
133
|
@current_entry = nil
|
130
134
|
@compressor = ::Zip::NullCompressor.instance
|
131
135
|
end
|
@@ -134,13 +138,14 @@ module Zip
|
|
134
138
|
finalize_current_entry
|
135
139
|
@entry_set << entry
|
136
140
|
entry.write_local_entry(@output_stream)
|
141
|
+
@encrypter.reset!
|
137
142
|
@compressor = get_compressor(entry, level)
|
138
143
|
end
|
139
144
|
|
140
145
|
def get_compressor(entry, level)
|
141
146
|
case entry.compression_method
|
142
147
|
when Entry::DEFLATED then
|
143
|
-
::Zip::Deflater.new(@output_stream, level)
|
148
|
+
::Zip::Deflater.new(@output_stream, level, @encrypter)
|
144
149
|
when Entry::STORED then
|
145
150
|
::Zip::PassThruCompressor.new(@output_stream)
|
146
151
|
else
|
data/lib/zip/version.rb
CHANGED
data/test/basic_zip_file_test.rb
CHANGED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class NullEncrypterTest < MiniTest::Test
|
4
|
+
def setup
|
5
|
+
@encrypter = ::Zip::NullEncrypter.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_header_bytesize
|
9
|
+
assert_equal 0, @encrypter.header_bytesize
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_gp_flags
|
13
|
+
assert_equal 0, @encrypter.gp_flags
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_header
|
17
|
+
assert_empty @encrypter.header(nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_encrypt
|
21
|
+
[nil, '', 'a' * 10, 0xffffffff].each do |data|
|
22
|
+
assert_equal data, @encrypter.encrypt(data)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_reset!
|
27
|
+
assert_respond_to @encrypter, :reset!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class NullDecrypterTest < MiniTest::Test
|
32
|
+
def setup
|
33
|
+
@decrypter = ::Zip::NullDecrypter.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_header_bytesize
|
37
|
+
assert_equal 0, @decrypter.header_bytesize
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_gp_flags
|
41
|
+
assert_equal 0, @decrypter.gp_flags
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_decrypt
|
45
|
+
[nil, '', 'a' * 10, 0xffffffff].each do |data|
|
46
|
+
assert_equal data, @decrypter.decrypt(data)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_reset!
|
51
|
+
assert_respond_to @decrypter, :reset!
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TraditionalEncrypterTest < MiniTest::Test
|
4
|
+
def setup
|
5
|
+
@mtime = ::Zip::DOSTime.new(2014, 12, 17, 15, 56, 24)
|
6
|
+
@encrypter = ::Zip::TraditionalEncrypter.new('password')
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_header_bytesize
|
10
|
+
assert_equal 12, @encrypter.header_bytesize
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_gp_flags
|
14
|
+
assert_equal 9, @encrypter.gp_flags
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_header
|
18
|
+
@encrypter.reset!
|
19
|
+
exepected = [239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 121, 91].pack("C*")
|
20
|
+
Random.stub(:rand, 1) do
|
21
|
+
assert_equal exepected, @encrypter.header(@mtime)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_encrypt
|
26
|
+
@encrypter.reset!
|
27
|
+
Random.stub(:rand, 1) { @encrypter.header(@mtime) }
|
28
|
+
assert_raises(NoMethodError) { @encrypter.encrypt(nil) }
|
29
|
+
assert_raises(NoMethodError) { @encrypter.encrypt(1) }
|
30
|
+
assert_equal '', @encrypter.encrypt('')
|
31
|
+
assert_equal [100, 218, 7, 114, 226, 82, 62, 93, 224, 62].pack("C*"), @encrypter.encrypt('a' * 10)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_reset!
|
35
|
+
@encrypter.reset!
|
36
|
+
Random.stub(:rand, 1) { @encrypter.header(@mtime) }
|
37
|
+
[100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c|
|
38
|
+
assert_equal c, @encrypter.encrypt('a')
|
39
|
+
end
|
40
|
+
assert_equal 56.chr, @encrypter.encrypt('a')
|
41
|
+
@encrypter.reset!
|
42
|
+
Random.stub(:rand, 1) { @encrypter.header(@mtime) }
|
43
|
+
[100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c|
|
44
|
+
assert_equal c, @encrypter.encrypt('a')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class TraditionalDecrypterTest < MiniTest::Test
|
50
|
+
def setup
|
51
|
+
@decrypter = ::Zip::TraditionalDecrypter.new('password')
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_header_bytesize
|
55
|
+
assert_equal 12, @decrypter.header_bytesize
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_gp_flags
|
59
|
+
assert_equal 9, @decrypter.gp_flags
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_decrypt
|
63
|
+
@decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 121, 91].pack("C*"))
|
64
|
+
[100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c|
|
65
|
+
assert_equal 'a', @decrypter.decrypt(c)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_reset!
|
70
|
+
@decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 121, 91].pack("C*"))
|
71
|
+
[100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c|
|
72
|
+
assert_equal 'a', @decrypter.decrypt(c)
|
73
|
+
end
|
74
|
+
assert_equal 91.chr, @decrypter.decrypt(2.chr)
|
75
|
+
@decrypter.reset!([239, 57, 234, 154, 246, 80, 83, 221, 74, 200, 121, 91].pack("C*"))
|
76
|
+
[100, 218, 7, 114, 226, 82, 62, 93, 224, 62].map(&:chr).each do |c|
|
77
|
+
assert_equal 'a', @decrypter.decrypt(c)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|