rubyzip 2.4.1 → 3.2.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +476 -0
  3. data/LICENSE.md +24 -0
  4. data/README.md +180 -40
  5. data/Rakefile +15 -13
  6. data/lib/zip/central_directory.rb +172 -124
  7. data/lib/zip/compressor.rb +3 -1
  8. data/lib/zip/constants.rb +29 -21
  9. data/lib/zip/crypto/aes_encryption.rb +120 -0
  10. data/lib/zip/crypto/decrypted_io.rb +20 -14
  11. data/lib/zip/crypto/encryption.rb +4 -2
  12. data/lib/zip/crypto/null_encryption.rb +5 -13
  13. data/lib/zip/crypto/traditional_encryption.rb +10 -6
  14. data/lib/zip/decompressor.rb +4 -3
  15. data/lib/zip/deflater.rb +12 -8
  16. data/lib/zip/dirtyable.rb +32 -0
  17. data/lib/zip/dos_time.rb +45 -5
  18. data/lib/zip/entry.rb +391 -264
  19. data/lib/zip/entry_set.rb +11 -9
  20. data/lib/zip/errors.rb +136 -16
  21. data/lib/zip/extra_field/aes.rb +50 -0
  22. data/lib/zip/extra_field/generic.rb +10 -11
  23. data/lib/zip/extra_field/ntfs.rb +6 -4
  24. data/lib/zip/extra_field/old_unix.rb +3 -1
  25. data/lib/zip/extra_field/universal_time.rb +3 -1
  26. data/lib/zip/extra_field/unix.rb +5 -3
  27. data/lib/zip/extra_field/unknown.rb +35 -0
  28. data/lib/zip/extra_field/zip64.rb +19 -5
  29. data/lib/zip/extra_field.rb +25 -23
  30. data/lib/zip/file.rb +174 -267
  31. data/lib/zip/file_split.rb +91 -0
  32. data/lib/zip/filesystem/dir.rb +86 -0
  33. data/lib/zip/filesystem/directory_iterator.rb +48 -0
  34. data/lib/zip/filesystem/file.rb +263 -0
  35. data/lib/zip/filesystem/file_stat.rb +110 -0
  36. data/lib/zip/filesystem/zip_file_name_mapper.rb +81 -0
  37. data/lib/zip/filesystem.rb +27 -596
  38. data/lib/zip/inflater.rb +11 -8
  39. data/lib/zip/input_stream.rb +76 -57
  40. data/lib/zip/ioextras/abstract_input_stream.rb +19 -13
  41. data/lib/zip/ioextras/abstract_output_stream.rb +13 -3
  42. data/lib/zip/ioextras.rb +7 -7
  43. data/lib/zip/null_compressor.rb +3 -1
  44. data/lib/zip/null_decompressor.rb +6 -3
  45. data/lib/zip/null_input_stream.rb +3 -1
  46. data/lib/zip/output_stream.rb +60 -57
  47. data/lib/zip/pass_thru_compressor.rb +3 -1
  48. data/lib/zip/pass_thru_decompressor.rb +8 -5
  49. data/lib/zip/streamable_directory.rb +3 -1
  50. data/lib/zip/streamable_stream.rb +4 -1
  51. data/lib/zip/version.rb +4 -1
  52. data/lib/zip.rb +25 -22
  53. data/rubyzip.gemspec +39 -0
  54. data/samples/example.rb +8 -3
  55. data/samples/example_filesystem.rb +3 -2
  56. data/samples/example_recursive.rb +3 -1
  57. data/samples/gtk_ruby_zip.rb +5 -3
  58. data/samples/qtzip.rb +7 -6
  59. data/samples/write_simple.rb +2 -1
  60. data/samples/zipfind.rb +1 -0
  61. metadata +86 -52
  62. data/TODO +0 -15
  63. data/lib/zip/extra_field/zip64_placeholder.rb +0 -15
@@ -1,45 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require_relative 'dirtyable'
6
+
1
7
  module Zip
2
- class CentralDirectory
3
- include Enumerable
8
+ class CentralDirectory # :nodoc:
9
+ extend Forwardable
10
+ include Dirtyable
11
+
12
+ END_OF_CD_SIG = 0x06054b50
13
+ ZIP64_END_OF_CD_SIG = 0x06064b50
14
+ ZIP64_EOCD_LOCATOR_SIG = 0x07064b50
4
15
 
5
- END_OF_CDS = 0x06054b50
6
- ZIP64_END_OF_CDS = 0x06064b50
7
- ZIP64_EOCD_LOCATOR = 0x07064b50
8
- MAX_END_OF_CDS_SIZE = 65_536 + 18
9
16
  STATIC_EOCD_SIZE = 22
17
+ ZIP64_STATIC_EOCD_SIZE = 56
18
+ ZIP64_EOCD_LOC_SIZE = 20
19
+ MAX_FILE_COMMENT_SIZE = (1 << 16) - 1
20
+ MAX_END_OF_CD_SIZE =
21
+ MAX_FILE_COMMENT_SIZE + STATIC_EOCD_SIZE + ZIP64_EOCD_LOC_SIZE
10
22
 
11
- attr_reader :comment
23
+ attr_accessor :comment
12
24
 
13
- # Returns an Enumerable containing the entries.
14
- def entries
15
- @entry_set.entries
16
- end
25
+ def_delegators :@entry_set,
26
+ :<<, :delete, :each, :entries, :find_entry, :glob,
27
+ :include?, :size
28
+
29
+ mark_dirty :<<, :comment=, :delete
17
30
 
18
- def initialize(entries = EntrySet.new, comment = '') #:nodoc:
19
- super()
31
+ def initialize(entries = EntrySet.new, comment = '') # :nodoc:
32
+ super(dirty_on_create: false)
20
33
  @entry_set = entries.kind_of?(EntrySet) ? entries : EntrySet.new(entries)
21
34
  @comment = comment
22
35
  end
23
36
 
24
- def write_to_stream(io) #:nodoc:
37
+ def read_from_stream(io)
38
+ read_eocds(io)
39
+ read_central_directory_entries(io)
40
+ end
41
+
42
+ def write_to_stream(io, suppress_extra_fields: false) # :nodoc:
25
43
  cdir_offset = io.tell
26
- @entry_set.each { |entry| entry.write_c_dir_entry(io) }
44
+ @entry_set.each do |entry|
45
+ entry.write_c_dir_entry(io, suppress_extra_fields: suppress_extra_fields)
46
+ end
27
47
  eocd_offset = io.tell
28
48
  cdir_size = eocd_offset - cdir_offset
29
- if ::Zip.write_zip64_support
30
- need_zip64_eocd = cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF
31
- need_zip64_eocd ||= @entry_set.any? { |entry| entry.extra['Zip64'] }
32
- if need_zip64_eocd
33
- write_64_e_o_c_d(io, cdir_offset, cdir_size)
34
- write_64_eocd_locator(io, eocd_offset)
35
- end
49
+ if Zip.write_zip64_support &&
50
+ (cdir_offset > 0xFFFFFFFF || cdir_size > 0xFFFFFFFF || @entry_set.size > 0xFFFF)
51
+ write_64_e_o_c_d(io, cdir_offset, cdir_size)
52
+ write_64_eocd_locator(io, eocd_offset)
36
53
  end
37
54
  write_e_o_c_d(io, cdir_offset, cdir_size)
38
55
  end
39
56
 
40
- def write_e_o_c_d(io, offset, cdir_size) #:nodoc:
57
+ # Reads the End of Central Directory Record (and the Zip64 equivalent if
58
+ # needs be) and returns the number of entries in the archive. This is a
59
+ # convenience method that avoids reading in all of the entry data to get a
60
+ # very quick entry count.
61
+ def count_entries(io)
62
+ read_eocds(io)
63
+ @size
64
+ end
65
+
66
+ def ==(other) # :nodoc:
67
+ return false unless other.kind_of?(CentralDirectory)
68
+
69
+ @entry_set.entries.sort == other.entries.sort && comment == other.comment
70
+ end
71
+
72
+ private
73
+
74
+ def write_e_o_c_d(io, offset, cdir_size) # :nodoc:
41
75
  tmp = [
42
- END_OF_CDS,
76
+ END_OF_CD_SIG,
43
77
  0, # @numberOfThisDisk
44
78
  0, # @numberOfDiskWithStartOfCDir
45
79
  @entry_set ? [@entry_set.size, 0xFFFF].min : 0,
@@ -52,11 +86,9 @@ module Zip
52
86
  io << @comment
53
87
  end
54
88
 
55
- private :write_e_o_c_d
56
-
57
- def write_64_e_o_c_d(io, offset, cdir_size) #:nodoc:
89
+ def write_64_e_o_c_d(io, offset, cdir_size) # :nodoc:
58
90
  tmp = [
59
- ZIP64_END_OF_CDS,
91
+ ZIP64_END_OF_CD_SIG,
60
92
  44, # size of zip64 end of central directory record (excludes signature and field itself)
61
93
  VERSION_MADE_BY,
62
94
  VERSION_NEEDED_TO_EXTRACT_ZIP64,
@@ -70,11 +102,9 @@ module Zip
70
102
  io << tmp.pack('VQ<vvVVQ<Q<Q<Q<')
71
103
  end
72
104
 
73
- private :write_64_e_o_c_d
74
-
75
105
  def write_64_eocd_locator(io, zip64_eocd_offset)
76
106
  tmp = [
77
- ZIP64_EOCD_LOCATOR,
107
+ ZIP64_EOCD_LOCATOR_SIG,
78
108
  0, # number of disk containing the start of zip64 eocd record
79
109
  zip64_eocd_offset, # offset of the start of zip64 eocd record in its disk
80
110
  1 # total number of disks
@@ -82,127 +112,145 @@ module Zip
82
112
  io << tmp.pack('VVQ<V')
83
113
  end
84
114
 
85
- private :write_64_eocd_locator
86
-
87
- def read_64_e_o_c_d(buf) #:nodoc:
88
- buf = get_64_e_o_c_d(buf)
89
- @size_of_zip64_e_o_c_d = Entry.read_zip_64_long(buf)
90
- @version_made_by = Entry.read_zip_short(buf)
91
- @version_needed_for_extract = Entry.read_zip_short(buf)
92
- @number_of_this_disk = Entry.read_zip_long(buf)
93
- @number_of_disk_with_start_of_cdir = Entry.read_zip_long(buf)
94
- @total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_64_long(buf)
95
- @size = Entry.read_zip_64_long(buf)
96
- @size_in_bytes = Entry.read_zip_64_long(buf)
97
- @cdir_offset = Entry.read_zip_64_long(buf)
98
- @zip_64_extensible = buf.slice!(0, buf.bytesize)
99
- raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
115
+ def unpack_64_e_o_c_d(buffer) # :nodoc:
116
+ _, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
117
+ @size_of_zip64_e_o_c_d,
118
+ @version_made_by,
119
+ @version_needed_for_extract,
120
+ @number_of_this_disk,
121
+ @number_of_disk_with_start_of_cdir,
122
+ @total_number_of_entries_in_cdir_on_this_disk,
123
+ @size,
124
+ @size_in_bytes,
125
+ @cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
126
+
127
+ zip64_extensible_data_size =
128
+ @size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
129
+ @zip64_extensible_data = if zip64_extensible_data_size.zero?
130
+ ''
131
+ else
132
+ buffer.slice(
133
+ ZIP64_STATIC_EOCD_SIZE,
134
+ zip64_extensible_data_size
135
+ )
136
+ end
137
+ end
138
+
139
+ def unpack_64_eocd_locator(buffer) # :nodoc:
140
+ _, # ZIP64_EOCD_LOCATOR_SIG. We know we have this at this point.
141
+ _, zip64_eocd_offset, = buffer.unpack('VVQ<V')
142
+
143
+ zip64_eocd_offset
100
144
  end
101
145
 
102
- def read_e_o_c_d(buf) #:nodoc:
103
- buf = get_e_o_c_d(buf)
104
- @number_of_this_disk = Entry.read_zip_short(buf)
105
- @number_of_disk_with_start_of_cdir = Entry.read_zip_short(buf)
106
- @total_number_of_entries_in_cdir_on_this_disk = Entry.read_zip_short(buf)
107
- @size = Entry.read_zip_short(buf)
108
- @size_in_bytes = Entry.read_zip_long(buf)
109
- @cdir_offset = Entry.read_zip_long(buf)
110
- comment_length = Entry.read_zip_short(buf)
111
- @comment = if comment_length.to_i <= 0
112
- buf.slice!(0, buf.size)
113
- else
114
- buf.read(comment_length)
115
- end
116
- raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
146
+ def unpack_e_o_c_d(buffer) # :nodoc:
147
+ _, # END_OF_CD_SIG. We know we have this at this point.
148
+ num_disk,
149
+ num_disk_cdir,
150
+ num_cdir_disk,
151
+ num_entries,
152
+ size_in_bytes,
153
+ cdir_offset,
154
+ comment_length = buffer.unpack('VvvvvVVv')
155
+
156
+ @number_of_this_disk = num_disk unless num_disk == 0xFFFF
157
+ @number_of_disk_with_start_of_cdir = num_disk_cdir unless num_disk_cdir == 0xFFFF
158
+ @total_number_of_entries_in_cdir_on_this_disk = num_cdir_disk unless num_cdir_disk == 0xFFFF
159
+ @size = num_entries unless num_entries == 0xFFFF
160
+ @size_in_bytes = size_in_bytes unless size_in_bytes == 0xFFFFFFFF
161
+ @cdir_offset = cdir_offset unless cdir_offset == 0xFFFFFFFF
162
+
163
+ @comment = if comment_length.positive?
164
+ buffer.slice(STATIC_EOCD_SIZE, comment_length)
165
+ else
166
+ ''
167
+ end
117
168
  end
118
169
 
119
- def read_central_directory_entries(io) #:nodoc:
170
+ def read_central_directory_entries(io) # :nodoc:
171
+ # `StringIO` doesn't raise `EINVAL` if you seek beyond the current end,
172
+ # so we need to catch that *and* query `io#eof?` here.
173
+ eof = false
120
174
  begin
121
175
  io.seek(@cdir_offset, IO::SEEK_SET)
122
176
  rescue Errno::EINVAL
123
- raise Error, 'Zip consistency problem while reading central directory entry'
177
+ eof = true
124
178
  end
179
+ raise Error, 'Zip consistency problem while reading central directory entry' if eof || io.eof?
180
+
125
181
  @entry_set = EntrySet.new
126
182
  @size.times do
127
- @entry_set << Entry.read_c_dir_entry(io)
128
- end
129
- end
183
+ entry = Entry.read_c_dir_entry(io)
184
+ next unless entry
185
+
186
+ offset = if entry.zip64?
187
+ entry.extra[:zip64].relative_header_offset
188
+ else
189
+ entry.local_header_offset
190
+ end
191
+
192
+ unless offset.nil?
193
+ io_save = io.tell
194
+ io.seek(offset, IO::SEEK_SET)
195
+ entry.read_extra_field(read_local_extra_field(io), local: true)
196
+ io.seek(io_save, IO::SEEK_SET)
197
+ end
130
198
 
131
- def read_from_stream(io) #:nodoc:
132
- buf = start_buf(io)
133
- if zip64_file?(buf)
134
- read_64_e_o_c_d(buf)
135
- else
136
- read_e_o_c_d(buf)
199
+ @entry_set << entry
137
200
  end
138
- read_central_directory_entries(io)
139
201
  end
140
202
 
141
- def get_e_o_c_d(buf) #:nodoc:
142
- sig_index = buf.rindex([END_OF_CDS].pack('V'))
143
- raise Error, 'Zip end of central directory signature not found' unless sig_index
203
+ def read_local_extra_field(io)
204
+ buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
205
+ return '' unless buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
144
206
 
145
- buf = buf.slice!((sig_index + 4)..(buf.bytesize))
207
+ head, _, _, _, _, _, _, _, _, _, n_len, e_len = buf.unpack('VCCvvvvVVVvv')
208
+ return '' unless head == ::Zip::LOCAL_ENTRY_SIGNATURE
146
209
 
147
- def buf.read(count)
148
- slice!(0, count)
149
- end
150
-
151
- buf
210
+ io.seek(n_len, IO::SEEK_CUR) # Skip over the entry name.
211
+ io.read(e_len)
152
212
  end
153
213
 
154
- def zip64_file?(buf)
155
- buf.rindex([ZIP64_END_OF_CDS].pack('V')) && buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
156
- end
214
+ def read_eocds(io) # :nodoc:
215
+ base_location, data = eocd_data(io)
157
216
 
158
- def start_buf(io)
159
- begin
160
- io.seek(-MAX_END_OF_CDS_SIZE, IO::SEEK_END)
161
- rescue Errno::EINVAL
162
- io.seek(0, IO::SEEK_SET)
163
- end
164
- io.read
165
- end
217
+ eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
218
+ raise Error, 'Zip end of central directory signature not found' unless eocd_location
166
219
 
167
- def get_64_e_o_c_d(buf) #:nodoc:
168
- zip_64_start = buf.rindex([ZIP64_END_OF_CDS].pack('V'))
169
- raise Error, 'Zip64 end of central directory signature not found' unless zip_64_start
220
+ zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'))
170
221
 
171
- zip_64_locator = buf.rindex([ZIP64_EOCD_LOCATOR].pack('V'))
172
- raise Error, 'Zip64 end of central directory signature locator not found' unless zip_64_locator
222
+ if zip64_eocd_locator
223
+ zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'))
173
224
 
174
- buf = buf.slice!((zip_64_start + 4)..zip_64_locator)
225
+ zip64_eocd_data =
226
+ if zip64_eocd_location
227
+ data.slice(zip64_eocd_location..zip64_eocd_locator)
228
+ else
229
+ zip64_eocd_location = unpack_64_eocd_locator(
230
+ data.slice(zip64_eocd_locator..eocd_location)
231
+ )
232
+ unless zip64_eocd_location
233
+ raise Error, 'Zip64 end of central directory signature not found'
234
+ end
175
235
 
176
- def buf.read(count)
177
- slice!(0, count)
178
- end
236
+ io.seek(zip64_eocd_location, IO::SEEK_SET)
237
+ io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
238
+ end
179
239
 
180
- buf
181
- end
182
-
183
- # For iterating over the entries.
184
- def each(&a_proc)
185
- @entry_set.each(&a_proc)
186
- end
187
-
188
- # Returns the number of entries in the central directory (and
189
- # consequently in the zip archive).
190
- def size
191
- @entry_set.size
192
- end
240
+ unpack_64_e_o_c_d(zip64_eocd_data)
241
+ end
193
242
 
194
- def self.read_from_stream(io) #:nodoc:
195
- cdir = new
196
- cdir.read_from_stream(io)
197
- cdir
198
- rescue Error
199
- nil
243
+ unpack_e_o_c_d(data.slice(eocd_location..-1))
200
244
  end
201
245
 
202
- def ==(other) #:nodoc:
203
- return false unless other.kind_of?(CentralDirectory)
246
+ def eocd_data(io)
247
+ begin
248
+ io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
249
+ rescue Errno::EINVAL
250
+ io.seek(0, IO::SEEK_SET)
251
+ end
204
252
 
205
- @entry_set.entries.sort == other.entries.sort && comment == other.comment
253
+ [io.tell, io.read]
206
254
  end
207
255
  end
208
256
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class Compressor #:nodoc:all
4
+ class Compressor # :nodoc:all
3
5
  def finish; end
4
6
  end
5
7
  end
data/lib/zip/constants.rb CHANGED
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
4
+ # :stopdoc:
5
+
2
6
  RUNNING_ON_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/i
3
7
 
4
8
  CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
@@ -11,6 +15,8 @@ module Zip
11
15
  VERSION_NEEDED_TO_EXTRACT = 20
12
16
  VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45
13
17
 
18
+ SPLIT_FILE_SIGNATURE = 0x08074b50
19
+
14
20
  FILE_TYPE_FILE = 0o10
15
21
  FILE_TYPE_DIR = 0o04
16
22
  FILE_TYPE_SYMLINK = 0o12
@@ -38,27 +44,27 @@ module Zip
38
44
  FSTYPE_ATHEOS = 30
39
45
 
40
46
  FSTYPES = {
41
- FSTYPE_FAT => 'FAT'.freeze,
42
- FSTYPE_AMIGA => 'Amiga'.freeze,
43
- FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze,
44
- FSTYPE_UNIX => 'Unix'.freeze,
45
- FSTYPE_VM_CMS => 'VM/CMS'.freeze,
46
- FSTYPE_ATARI => 'Atari ST'.freeze,
47
- FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze,
48
- FSTYPE_MAC => 'Macintosh'.freeze,
49
- FSTYPE_Z_SYSTEM => 'Z-System'.freeze,
50
- FSTYPE_CPM => 'CP/M'.freeze,
51
- FSTYPE_TOPS20 => 'TOPS-20'.freeze,
52
- FSTYPE_NTFS => 'NTFS'.freeze,
53
- FSTYPE_QDOS => 'SMS/QDOS'.freeze,
54
- FSTYPE_ACORN => 'Acorn RISC OS'.freeze,
55
- FSTYPE_VFAT => 'Win32 VFAT'.freeze,
56
- FSTYPE_MVS => 'MVS'.freeze,
57
- FSTYPE_BEOS => 'BeOS'.freeze,
58
- FSTYPE_TANDEM => 'Tandem NSK'.freeze,
59
- FSTYPE_THEOS => 'Theos'.freeze,
60
- FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze,
61
- FSTYPE_ATHEOS => 'AtheOS'.freeze
47
+ FSTYPE_FAT => 'FAT',
48
+ FSTYPE_AMIGA => 'Amiga',
49
+ FSTYPE_VMS => 'VMS (Vax or Alpha AXP)',
50
+ FSTYPE_UNIX => 'Unix',
51
+ FSTYPE_VM_CMS => 'VM/CMS',
52
+ FSTYPE_ATARI => 'Atari ST',
53
+ FSTYPE_HPFS => 'OS/2 or NT HPFS',
54
+ FSTYPE_MAC => 'Macintosh',
55
+ FSTYPE_Z_SYSTEM => 'Z-System',
56
+ FSTYPE_CPM => 'CP/M',
57
+ FSTYPE_TOPS20 => 'TOPS-20',
58
+ FSTYPE_NTFS => 'NTFS',
59
+ FSTYPE_QDOS => 'SMS/QDOS',
60
+ FSTYPE_ACORN => 'Acorn RISC OS',
61
+ FSTYPE_VFAT => 'Win32 VFAT',
62
+ FSTYPE_MVS => 'MVS',
63
+ FSTYPE_BEOS => 'BeOS',
64
+ FSTYPE_TANDEM => 'Tandem NSK',
65
+ FSTYPE_THEOS => 'Theos',
66
+ FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)',
67
+ FSTYPE_ATHEOS => 'AtheOS'
62
68
  }.freeze
63
69
 
64
70
  COMPRESSION_METHOD_STORE = 0
@@ -112,4 +118,6 @@ module Zip
112
118
  COMPRESSION_METHOD_PPMD => 'PPMd version I, Rev 1',
113
119
  COMPRESSION_METHOD_AES => 'AES encryption'
114
120
  }.freeze
121
+
122
+ # :startdoc:
115
123
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Zip
6
+ module AESEncryption # :nodoc:
7
+ VERIFIER_LENGTH = 2
8
+ BLOCK_SIZE = 16
9
+ AUTHENTICATION_CODE_LENGTH = 10
10
+
11
+ VERSION_AE_1 = 0x01
12
+ VERSION_AE_2 = 0x02
13
+
14
+ VERSIONS = [
15
+ VERSION_AE_1,
16
+ VERSION_AE_2
17
+ ].freeze
18
+
19
+ STRENGTH_128_BIT = 0x01
20
+ STRENGTH_192_BIT = 0x02
21
+ STRENGTH_256_BIT = 0x03
22
+
23
+ STRENGTHS = [
24
+ STRENGTH_128_BIT,
25
+ STRENGTH_192_BIT,
26
+ STRENGTH_256_BIT
27
+ ].freeze
28
+
29
+ BITS = {
30
+ STRENGTH_128_BIT => 128,
31
+ STRENGTH_192_BIT => 192,
32
+ STRENGTH_256_BIT => 256
33
+ }.freeze
34
+
35
+ KEY_LENGTHS = {
36
+ STRENGTH_128_BIT => 16,
37
+ STRENGTH_192_BIT => 24,
38
+ STRENGTH_256_BIT => 32
39
+ }.freeze
40
+
41
+ SALT_LENGTHS = {
42
+ STRENGTH_128_BIT => 8,
43
+ STRENGTH_192_BIT => 12,
44
+ STRENGTH_256_BIT => 16
45
+ }.freeze
46
+
47
+ def initialize(password, strength)
48
+ @password = password
49
+ @strength = strength
50
+ @bits = BITS[@strength]
51
+ @key_length = KEY_LENGTHS[@strength]
52
+ @salt_length = SALT_LENGTHS[@strength]
53
+ end
54
+
55
+ def header_bytesize
56
+ @salt_length + VERIFIER_LENGTH
57
+ end
58
+
59
+ def gp_flags
60
+ 0x0001
61
+ end
62
+ end
63
+
64
+ class AESDecrypter < Decrypter # :nodoc:
65
+ include AESEncryption
66
+
67
+ def decrypt(encrypted_data)
68
+ @hmac.update(encrypted_data)
69
+
70
+ idx = 0
71
+ decrypted_data = +''
72
+ amount_to_read = encrypted_data.size
73
+
74
+ while amount_to_read.positive?
75
+ @cipher.iv = [@counter + 1].pack('Vx12')
76
+ begin_index = BLOCK_SIZE * idx
77
+ end_index = begin_index + [BLOCK_SIZE, amount_to_read].min
78
+ decrypted_data << @cipher.update(encrypted_data[begin_index...end_index])
79
+ amount_to_read -= BLOCK_SIZE
80
+ @counter += 1
81
+ idx += 1
82
+ end
83
+
84
+ # JRuby requires finalization of the cipher. This is a bug, as noted in
85
+ # jruby/jruby-openssl#182 and jruby/jruby-openssl#183.
86
+ decrypted_data << @cipher.final if defined?(JRUBY_VERSION)
87
+ decrypted_data
88
+ end
89
+
90
+ def reset!(header)
91
+ raise Error, "Unsupported encryption AES-#{@bits}" unless STRENGTHS.include? @strength
92
+
93
+ salt = header[0...@salt_length]
94
+ pwd_verify = header[-VERIFIER_LENGTH..]
95
+ key_material = OpenSSL::KDF.pbkdf2_hmac(
96
+ @password,
97
+ salt: salt,
98
+ iterations: 1000,
99
+ length: (2 * @key_length) + VERIFIER_LENGTH,
100
+ hash: 'sha1'
101
+ )
102
+ enc_key = key_material[0...@key_length]
103
+ enc_hmac_key = key_material[@key_length...(2 * @key_length)]
104
+ enc_pwd_verify = key_material[-VERIFIER_LENGTH..]
105
+
106
+ raise Error, 'Bad password' if enc_pwd_verify != pwd_verify
107
+
108
+ @counter = 0
109
+ @cipher = OpenSSL::Cipher::AES.new(@bits, :CTR)
110
+ @cipher.decrypt
111
+ @cipher.key = enc_key
112
+ @hmac = OpenSSL::HMAC.new(enc_hmac_key, OpenSSL::Digest.new('SHA1'))
113
+ end
114
+
115
+ def check_integrity!(io)
116
+ auth_code = io.read(AUTHENTICATION_CODE_LENGTH)
117
+ raise Error, 'Integrity fault' if @hmac.digest[0...AUTHENTICATION_CODE_LENGTH] != auth_code
118
+ end
119
+ end
120
+ end
@@ -1,40 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class DecryptedIo #:nodoc:all
4
+ class DecryptedIo # :nodoc:all
3
5
  CHUNK_SIZE = 32_768
4
6
 
5
- def initialize(io, decrypter)
7
+ def initialize(io, decrypter, compressed_size)
6
8
  @io = io
7
9
  @decrypter = decrypter
10
+ @bytes_remaining = compressed_size
11
+ @buffer = +''
8
12
  end
9
13
 
10
14
  def read(length = nil, outbuf = +'')
11
- return (length.nil? || length.zero? ? '' : nil) if eof
15
+ return (length.nil? || length.zero? ? '' : nil) if eof?
12
16
 
13
- while length.nil? || (buffer.bytesize < length)
17
+ while length.nil? || (@buffer.bytesize < length)
14
18
  break if input_finished?
15
19
 
16
- buffer << produce_input
20
+ @buffer << produce_input
17
21
  end
18
22
 
19
- outbuf.replace(buffer.slice!(0...(length || output_buffer.bytesize)))
23
+ @decrypter.check_integrity!(@io) if input_finished?
24
+
25
+ outbuf.replace(@buffer.slice!(0...(length || @buffer.bytesize)))
20
26
  end
21
27
 
22
28
  private
23
29
 
24
- def eof
25
- buffer.empty? && input_finished?
26
- end
27
-
28
- def buffer
29
- @buffer ||= +''
30
+ def eof?
31
+ @buffer.empty? && input_finished?
30
32
  end
31
33
 
32
34
  def input_finished?
33
- @io.eof
35
+ !@bytes_remaining.positive?
34
36
  end
35
37
 
36
38
  def produce_input
37
- @decrypter.decrypt(@io.read(CHUNK_SIZE))
39
+ chunk_size = [@bytes_remaining, CHUNK_SIZE].min
40
+ return '' unless chunk_size.positive?
41
+
42
+ @bytes_remaining -= chunk_size
43
+ @decrypter.decrypt(@io.read(chunk_size))
38
44
  end
39
45
  end
40
46
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zip
2
- class Encrypter #:nodoc:all
4
+ class Encrypter # :nodoc:all
3
5
  end
4
6
 
5
- class Decrypter
7
+ class Decrypter # :nodoc:all
6
8
  end
7
9
  end
8
10