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.

Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -13
  3. data/lib/zip.rb +5 -1
  4. data/lib/zip/central_directory.rb +1 -1
  5. data/lib/zip/crypto/encryption.rb +11 -0
  6. data/lib/zip/crypto/null_encryption.rb +45 -0
  7. data/lib/zip/crypto/traditional_encryption.rb +99 -0
  8. data/lib/zip/deflater.rb +6 -3
  9. data/lib/zip/entry.rb +11 -5
  10. data/lib/zip/entry_set.rb +5 -6
  11. data/lib/zip/extra_field.rb +1 -0
  12. data/lib/zip/extra_field/ntfs.rb +92 -0
  13. data/lib/zip/file.rb +11 -1
  14. data/lib/zip/filesystem.rb +4 -0
  15. data/lib/zip/inflater.rb +4 -3
  16. data/lib/zip/input_stream.rb +10 -4
  17. data/lib/zip/output_stream.rb +12 -7
  18. data/lib/zip/version.rb +1 -1
  19. data/test/basic_zip_file_test.rb +1 -1
  20. data/test/central_directory_entry_test.rb +1 -1
  21. data/test/central_directory_test.rb +5 -1
  22. data/test/crypto/null_encryption_test.rb +53 -0
  23. data/test/crypto/traditional_encryption_test.rb +80 -0
  24. data/test/data/WarnInvalidDate.zip +0 -0
  25. data/test/data/ntfs.zip +0 -0
  26. data/test/data/zipWithEncryption.zip +0 -0
  27. data/test/deflater_test.rb +14 -9
  28. data/test/encryption_test.rb +42 -0
  29. data/test/entry_set_test.rb +14 -1
  30. data/test/entry_test.rb +2 -2
  31. data/test/errors_test.rb +1 -1
  32. data/test/extra_field_test.rb +10 -1
  33. data/test/file_extract_directory_test.rb +3 -2
  34. data/test/file_extract_test.rb +2 -2
  35. data/test/file_split_test.rb +2 -2
  36. data/test/file_test.rb +16 -13
  37. data/test/filesystem/dir_iterator_test.rb +1 -1
  38. data/test/filesystem/directory_test.rb +1 -1
  39. data/test/filesystem/file_mutating_test.rb +1 -1
  40. data/test/filesystem/file_nonmutating_test.rb +10 -1
  41. data/test/filesystem/file_stat_test.rb +1 -1
  42. data/test/inflater_test.rb +1 -1
  43. data/test/input_stream_test.rb +1 -1
  44. data/test/ioextras/abstract_input_stream_test.rb +1 -1
  45. data/test/ioextras/abstract_output_stream_test.rb +1 -1
  46. data/test/ioextras/fake_io_test.rb +1 -1
  47. data/test/local_entry_test.rb +14 -11
  48. data/test/output_stream_test.rb +18 -3
  49. data/test/pass_thru_compressor_test.rb +2 -2
  50. data/test/pass_thru_decompressor_test.rb +1 -1
  51. data/test/settings_test.rb +23 -2
  52. data/test/test_helper.rb +1 -1
  53. data/test/unicode_file_names_and_comments_test.rb +16 -4
  54. data/test/zip64_full_test.rb +5 -1
  55. data/test/zip64_support_test.rb +1 -1
  56. metadata +104 -6
  57. 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
@@ -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
- zf.write_buffer(io)
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
@@ -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
@@ -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
@@ -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
- ::Zip::Inflater.new(@archive_io)
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}"
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Zip
2
- VERSION = '1.1.6'
2
+ VERSION = '1.1.7'
3
3
  end
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
- class BasicZipFileTest < MiniTest::Unit::TestCase
3
+ class BasicZipFileTest < MiniTest::Test
4
4
  include AssertEntry
5
5
 
6
6
  def setup
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
- class ZipCentralDirectoryEntryTest < MiniTest::Unit::TestCase
3
+ class ZipCentralDirectoryEntryTest < MiniTest::Test
4
4
 
5
5
  def test_read_from_stream
6
6
  File.open("test/data/testDirectory.bin", "rb") {
@@ -1,6 +1,10 @@
1
1
  require 'test_helper'
2
2
 
3
- class ZipCentralDirectoryTest < MiniTest::Unit::TestCase
3
+ class ZipCentralDirectoryTest < MiniTest::Test
4
+
5
+ def teardown
6
+ ::Zip.reset!
7
+ end
4
8
 
5
9
  def test_read_from_stream
6
10
  ::File.open(TestZipFile::TEST_ZIP2.zip_name, "rb") {
@@ -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