rubyzip 1.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.
Files changed (52) hide show
  1. checksums.yaml +6 -14
  2. data/README.md +231 -46
  3. data/Rakefile +13 -5
  4. data/TODO +0 -1
  5. data/lib/zip/central_directory.rb +64 -29
  6. data/lib/zip/compressor.rb +1 -2
  7. data/lib/zip/constants.rb +59 -5
  8. data/lib/zip/crypto/decrypted_io.rb +40 -0
  9. data/lib/zip/crypto/encryption.rb +11 -0
  10. data/lib/zip/crypto/null_encryption.rb +43 -0
  11. data/lib/zip/crypto/traditional_encryption.rb +99 -0
  12. data/lib/zip/decompressor.rb +22 -4
  13. data/lib/zip/deflater.rb +11 -6
  14. data/lib/zip/dos_time.rb +27 -16
  15. data/lib/zip/entry.rb +299 -163
  16. data/lib/zip/entry_set.rb +22 -20
  17. data/lib/zip/errors.rb +17 -6
  18. data/lib/zip/extra_field/generic.rb +15 -14
  19. data/lib/zip/extra_field/ntfs.rb +94 -0
  20. data/lib/zip/extra_field/old_unix.rb +46 -0
  21. data/lib/zip/extra_field/universal_time.rb +46 -16
  22. data/lib/zip/extra_field/unix.rb +10 -9
  23. data/lib/zip/extra_field/zip64.rb +46 -6
  24. data/lib/zip/extra_field/zip64_placeholder.rb +15 -0
  25. data/lib/zip/extra_field.rb +32 -18
  26. data/lib/zip/file.rb +260 -160
  27. data/lib/zip/filesystem.rb +297 -276
  28. data/lib/zip/inflater.rb +23 -34
  29. data/lib/zip/input_stream.rb +130 -82
  30. data/lib/zip/ioextras/abstract_input_stream.rb +36 -22
  31. data/lib/zip/ioextras/abstract_output_stream.rb +4 -6
  32. data/lib/zip/ioextras.rb +4 -6
  33. data/lib/zip/null_compressor.rb +2 -2
  34. data/lib/zip/null_decompressor.rb +4 -12
  35. data/lib/zip/null_input_stream.rb +2 -1
  36. data/lib/zip/output_stream.rb +75 -45
  37. data/lib/zip/pass_thru_compressor.rb +6 -6
  38. data/lib/zip/pass_thru_decompressor.rb +14 -24
  39. data/lib/zip/streamable_directory.rb +3 -3
  40. data/lib/zip/streamable_stream.rb +21 -16
  41. data/lib/zip/version.rb +1 -1
  42. data/lib/zip.rb +42 -3
  43. data/samples/example.rb +30 -40
  44. data/samples/example_filesystem.rb +16 -18
  45. data/samples/example_recursive.rb +33 -28
  46. data/samples/gtk_ruby_zip.rb +84 -0
  47. data/samples/qtzip.rb +25 -34
  48. data/samples/write_simple.rb +10 -13
  49. data/samples/zipfind.rb +38 -45
  50. metadata +129 -28
  51. data/NEWS +0 -182
  52. data/samples/gtkRubyzip.rb +0 -86
data/lib/zip/entry_set.rb CHANGED
@@ -5,8 +5,7 @@ module Zip
5
5
 
6
6
  def initialize(an_enumerable = [])
7
7
  super()
8
- @entry_set = {}
9
- @entry_order = []
8
+ @entry_set = {}
10
9
  an_enumerable.each { |o| push(o) }
11
10
  end
12
11
 
@@ -19,54 +18,50 @@ module Zip
19
18
  end
20
19
 
21
20
  def <<(entry)
22
- @entry_order.delete(to_key(entry))
23
- @entry_order << to_key(entry)
24
- @entry_set[to_key(entry)] = entry
21
+ @entry_set[to_key(entry)] = entry if entry
25
22
  end
26
23
 
27
- alias :push :<<
24
+ alias push <<
28
25
 
29
26
  def size
30
27
  @entry_set.size
31
28
  end
32
29
 
33
- alias :length :size
30
+ alias length size
34
31
 
35
32
  def delete(entry)
36
- if @entry_order.delete(to_key(entry)) && @entry_set.delete(to_key(entry))
37
- entry
38
- else
39
- nil
40
- end
33
+ entry if @entry_set.delete(to_key(entry))
41
34
  end
42
35
 
43
- def each(&block)
44
- @entry_order.each do |key|
45
- block.call @entry_set[key]
36
+ def each
37
+ @entry_set = sorted_entries.dup.each do |_, value|
38
+ yield(value)
46
39
  end
47
40
  end
48
41
 
49
42
  def entries
50
- @entry_order.map { |key| @entry_set[key] }
43
+ sorted_entries.values
51
44
  end
52
45
 
53
46
  # deep clone
54
47
  def dup
55
- EntrySet.new(@entry_order.map { |key| @entry_set[key].dup })
48
+ EntrySet.new(@entry_set.values.map(&:dup))
56
49
  end
57
50
 
58
51
  def ==(other)
59
52
  return false unless other.kind_of?(EntrySet)
60
- @entry_set == other.entry_set && @entry_order == other.entry_order
53
+
54
+ @entry_set.values == other.entry_set.values
61
55
  end
62
56
 
63
57
  def parent(entry)
64
58
  @entry_set[to_key(entry.parent_as_string)]
65
59
  end
66
60
 
67
- def glob(pattern, flags = ::File::FNM_PATHNAME|::File::FNM_DOTMATCH)
61
+ def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
68
62
  entries.map do |entry|
69
63
  next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
64
+
70
65
  yield(entry) if block_given?
71
66
  entry
72
67
  end.compact
@@ -74,9 +69,16 @@ module Zip
74
69
 
75
70
  protected
76
71
 
72
+ def sorted_entries
73
+ ::Zip.sort_entries ? Hash[@entry_set.sort] : @entry_set
74
+ end
75
+
77
76
  private
77
+
78
78
  def to_key(entry)
79
- entry.to_s.sub(/\/$/, '')
79
+ k = entry.to_s.chomp('/')
80
+ k.downcase! if ::Zip.case_insensitive_match
81
+ k
80
82
  end
81
83
  end
82
84
  end
data/lib/zip/errors.rb CHANGED
@@ -1,8 +1,19 @@
1
1
  module Zip
2
- class ZipError < StandardError; end
3
- class ZipEntryExistsError < ZipError; end
4
- class ZipDestinationFileExistsError < ZipError; end
5
- class ZipCompressionMethodError < ZipError; end
6
- class ZipEntryNameError < ZipError; end
7
- class ZipInternalError < ZipError; end
2
+ class Error < StandardError; end
3
+ class EntryExistsError < Error; end
4
+ class DestinationFileExistsError < Error; end
5
+ class CompressionMethodError < Error; end
6
+ class EntryNameError < Error; end
7
+ class EntrySizeError < Error; end
8
+ class InternalError < Error; end
9
+ class GPFBit3Error < Error; end
10
+ class DecompressionError < Error; end
11
+
12
+ # Backwards compatibility with v1 (delete in v2)
13
+ ZipError = Error
14
+ ZipEntryExistsError = EntryExistsError
15
+ ZipDestinationFileExistsError = DestinationFileExistsError
16
+ ZipCompressionMethodError = CompressionMethodError
17
+ ZipEntryNameError = EntryNameError
18
+ ZipInternalError = InternalError
8
19
  end
@@ -1,43 +1,44 @@
1
1
  module Zip
2
2
  class ExtraField::Generic
3
3
  def self.register_map
4
- if self.const_defined?(:HEADER_ID)
5
- ::Zip::ExtraField::ID_MAP[self.const_get(:HEADER_ID)] = self
6
- end
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
10
- self.to_s.split("::")[-1]
10
+ @name ||= to_s.split('::')[-1]
11
11
  end
12
12
 
13
13
  # return field [size, content] or false
14
14
  def initial_parse(binstr)
15
- if !binstr
16
- # If nil, start with empty.
17
- return false
18
- elsif binstr[0, 2] != self.class.const_get(:HEADER_ID)
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
- [binstr[2, 2].unpack("v")[0], binstr[4..-1]]
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
- v != other[k] and return false
29
+ return false if v != other[k]
29
30
  end
30
31
  true
31
32
  end
32
33
 
33
34
  def to_local_bin
34
35
  s = pack_for_local
35
- self.class.const_get(:HEADER_ID) + [s.bytesize].pack("v") + s
36
+ self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
36
37
  end
37
38
 
38
39
  def to_c_dir_bin
39
40
  s = pack_for_c_dir
40
- self.class.const_get(:HEADER_ID) + [s.bytesize].pack("v") + s
41
+ self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v') << s
41
42
  end
42
43
  end
43
- end
44
+ end
@@ -0,0 +1,94 @@
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 = 10_000_000.0
9
+ SEC_TO_UNIX_EPOCH = 11_644_473_600
10
+
11
+ def initialize(binstr = nil)
12
+ @ctime = nil
13
+ @mtime = nil
14
+ @atime = nil
15
+ binstr && merge(binstr)
16
+ end
17
+
18
+ attr_accessor :atime, :ctime, :mtime
19
+
20
+ def merge(binstr)
21
+ return if binstr.empty?
22
+
23
+ size, content = initial_parse(binstr)
24
+ (size && content) || return
25
+
26
+ content = content[4..-1]
27
+ tags = parse_tags(content)
28
+
29
+ tag1 = tags[1]
30
+ return unless tag1
31
+
32
+ ntfs_mtime, ntfs_atime, ntfs_ctime = tag1.unpack('Q<Q<Q<')
33
+ ntfs_mtime && @mtime ||= from_ntfs_time(ntfs_mtime)
34
+ ntfs_atime && @atime ||= from_ntfs_time(ntfs_atime)
35
+ ntfs_ctime && @ctime ||= from_ntfs_time(ntfs_ctime)
36
+ end
37
+
38
+ def ==(other)
39
+ @mtime == other.mtime &&
40
+ @atime == other.atime &&
41
+ @ctime == other.ctime
42
+ end
43
+
44
+ # Info-ZIP note states this extra field is stored at local header
45
+ def pack_for_local
46
+ pack_for_c_dir
47
+ end
48
+
49
+ # But 7-zip for Windows only stores at central dir
50
+ def pack_for_c_dir
51
+ # reserved 0 and tag 1
52
+ s = [0, 1].pack('Vv')
53
+
54
+ tag1 = ''.b
55
+ if @mtime
56
+ tag1 << [to_ntfs_time(@mtime)].pack('Q<')
57
+ if @atime
58
+ tag1 << [to_ntfs_time(@atime)].pack('Q<')
59
+ tag1 << [to_ntfs_time(@ctime)].pack('Q<') if @ctime
60
+ end
61
+ end
62
+ s << [tag1.bytesize].pack('v') << tag1
63
+ s
64
+ end
65
+
66
+ private
67
+
68
+ def parse_tags(content)
69
+ return {} if content.nil?
70
+
71
+ tags = {}
72
+ i = 0
73
+ while i < content.bytesize
74
+ tag, size = content[i, 4].unpack('vv')
75
+ i += 4
76
+ break unless tag && size
77
+
78
+ value = content[i, size]
79
+ i += size
80
+ tags[tag] = value
81
+ end
82
+
83
+ tags
84
+ end
85
+
86
+ def from_ntfs_time(ntfs_time)
87
+ ::Zip::DOSTime.at(ntfs_time / WINDOWS_TICK - SEC_TO_UNIX_EPOCH)
88
+ end
89
+
90
+ def to_ntfs_time(time)
91
+ ((time.to_f + SEC_TO_UNIX_EPOCH) * WINDOWS_TICK).to_i
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,46 @@
1
+ module Zip
2
+ # Olf Info-ZIP Extra for UNIX uid/gid and file timestampes
3
+ class ExtraField::OldUnix < ExtraField::Generic
4
+ HEADER_ID = 'UX'
5
+ register_map
6
+
7
+ def initialize(binstr = nil)
8
+ @uid = 0
9
+ @gid = 0
10
+ @atime = nil
11
+ @mtime = nil
12
+ binstr && merge(binstr)
13
+ end
14
+
15
+ attr_accessor :uid, :gid, :atime, :mtime
16
+
17
+ def merge(binstr)
18
+ return if binstr.empty?
19
+
20
+ size, content = initial_parse(binstr)
21
+ # size: 0 for central directory. 4 for local header
22
+ return if !size || size == 0
23
+
24
+ atime, mtime, uid, gid = content.unpack('VVvv')
25
+ @uid ||= uid
26
+ @gid ||= gid
27
+ @atime ||= atime
28
+ @mtime ||= mtime # rubocop:disable Naming/MemoizedInstanceVariableName
29
+ end
30
+
31
+ def ==(other)
32
+ @uid == other.uid &&
33
+ @gid == other.gid &&
34
+ @atime == other.atime &&
35
+ @mtime == other.mtime
36
+ end
37
+
38
+ def pack_for_local
39
+ [@atime, @mtime, @uid, @gid].pack('VVvv')
40
+ end
41
+
42
+ def pack_for_c_dir
43
+ [@atime, @mtime].pack('VV')
44
+ end
45
+ end
46
+ end
@@ -1,27 +1,57 @@
1
1
  module Zip
2
2
  # Info-ZIP Additional timestamp field
3
3
  class ExtraField::UniversalTime < ExtraField::Generic
4
- HEADER_ID = "UT"
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 = nil
12
- binstr and merge(binstr)
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
- attr_accessor :atime, :ctime, :mtime, :flag
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 or return
21
- @flag, mtime, atime, ctime = content.unpack("CVVV")
22
- mtime and @mtime ||= ::Zip::DOSTime.at(mtime)
23
- atime and @atime ||= ::Zip::DOSTime.at(atime)
24
- ctime and @ctime ||= ::Zip::DOSTime.at(ctime)
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)
@@ -31,17 +61,17 @@ module Zip
31
61
  end
32
62
 
33
63
  def pack_for_local
34
- s = [@flag].pack("C")
35
- @flag & 1 != 0 and s << [@mtime.to_i].pack("V")
36
- @flag & 2 != 0 and s << [@atime.to_i].pack("V")
37
- @flag & 4 != 0 and s << [@ctime.to_i].pack("V")
64
+ s = [@flag].pack('C')
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
- s = [@flag].pack("C")
43
- @flag & 1 == 1 and s << [@mtime.to_i].pack("V")
72
+ s = [@flag].pack('C')
73
+ s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
44
74
  s
45
75
  end
46
76
  end
47
- end
77
+ end
@@ -1,25 +1,27 @@
1
1
  module Zip
2
2
  # Info-ZIP Extra for UNIX uid/gid
3
3
  class ExtraField::IUnix < ExtraField::Generic
4
- HEADER_ID = "Ux"
4
+ HEADER_ID = 'Ux'
5
5
  register_map
6
6
 
7
7
  def initialize(binstr = nil)
8
8
  @uid = 0
9
9
  @gid = 0
10
- binstr and merge(binstr)
10
+ binstr && merge(binstr)
11
11
  end
12
12
 
13
13
  attr_accessor :uid, :gid
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
- return if (!size || size == 0)
20
- uid, gid = content.unpack("vv")
21
- @uid ||= uid
22
- @gid ||= gid
20
+ return if !size || size == 0
21
+
22
+ uid, gid = content.unpack('vv')
23
+ @uid ||= uid
24
+ @gid ||= gid # rubocop:disable Naming/MemoizedInstanceVariableName
23
25
  end
24
26
 
25
27
  def ==(other)
@@ -27,12 +29,11 @@ module Zip
27
29
  end
28
30
 
29
31
  def pack_for_local
30
- [@uid, @gid].pack("vv")
32
+ [@uid, @gid].pack('vv')
31
33
  end
32
34
 
33
35
  def pack_for_c_dir
34
36
  ''
35
37
  end
36
38
  end
37
-
38
- end
39
+ end
@@ -2,29 +2,69 @@ module Zip
2
2
  # Info-ZIP Extra for Zip64 size
3
3
  class ExtraField::Zip64 < ExtraField::Generic
4
4
  attr_accessor :original_size, :compressed_size, :relative_header_offset, :disk_start_number
5
- HEADER_ID = "\001\000"
5
+ HEADER_ID = ['0100'].pack('H*')
6
6
  register_map
7
7
 
8
8
  def initialize(binstr = nil)
9
+ # unparsed binary; we don't actually know what this contains
10
+ # without looking for FFs in the associated file header
11
+ # call parse after initializing with a binary string
12
+ @content = nil
9
13
  @original_size = nil
10
14
  @compressed_size = nil
11
15
  @relative_header_offset = nil
12
16
  @disk_start_number = nil
13
- binstr and merge(binstr)
17
+ binstr && merge(binstr)
18
+ end
19
+
20
+ def ==(other)
21
+ other.original_size == @original_size &&
22
+ other.compressed_size == @compressed_size &&
23
+ other.relative_header_offset == @relative_header_offset &&
24
+ other.disk_start_number == @disk_start_number
14
25
  end
15
26
 
16
27
  def merge(binstr)
17
28
  return if binstr.empty?
18
- id, size, @original_size, @compressed_size, @relative_header_offset, @disk_start_number = binstr.to_s.unpack("vvQQQV")
29
+
30
+ _, @content = initial_parse(binstr)
31
+ end
32
+
33
+ # pass the values from the base entry (if applicable)
34
+ # wider values are only present in the extra field for base values set to all FFs
35
+ # returns the final values for the four attributes (from the base or zip64 extra record)
36
+ def parse(original_size, compressed_size, relative_header_offset = nil, disk_start_number = nil)
37
+ @original_size = extract(8, 'Q<') if original_size == 0xFFFFFFFF
38
+ @compressed_size = extract(8, 'Q<') if compressed_size == 0xFFFFFFFF
39
+ @relative_header_offset = extract(8, 'Q<') if relative_header_offset && relative_header_offset == 0xFFFFFFFF
40
+ @disk_start_number = extract(4, 'V') if disk_start_number && disk_start_number == 0xFFFF
41
+ @content = nil
42
+ [@original_size || original_size,
43
+ @compressed_size || compressed_size,
44
+ @relative_header_offset || relative_header_offset,
45
+ @disk_start_number || disk_start_number]
46
+ end
47
+
48
+ def extract(size, format)
49
+ @content.slice!(0, size).unpack1(format)
19
50
  end
51
+ private :extract
20
52
 
21
53
  def pack_for_local
22
- return '' unless @original_size && @compressed_sie && @relative_header_offset && @disk_start_number
23
- [1, 16, @original_size, @compressed_size, @relative_header_offset, @disk_start_number].pack("vvQQQV")
54
+ # local header entries must contain original size and compressed size; other fields do not apply
55
+ return '' unless @original_size && @compressed_size
56
+
57
+ [@original_size, @compressed_size].pack('Q<Q<')
24
58
  end
25
59
 
26
60
  def pack_for_c_dir
27
- pack_for_local
61
+ # central directory entries contain only fields that didn't fit in the main entry part
62
+ packed = ''.b
63
+ packed << [@original_size].pack('Q<') if @original_size
64
+ packed << [@compressed_size].pack('Q<') if @compressed_size
65
+ packed << [@relative_header_offset].pack('Q<') if @relative_header_offset
66
+ packed << [@disk_start_number].pack('V') if @disk_start_number
67
+ packed
28
68
  end
29
69
  end
30
70
  end
@@ -0,0 +1,15 @@
1
+ module Zip
2
+ # placeholder to reserve space for a Zip64 extra information record, for the
3
+ # local file header only, that we won't know if we'll need until after
4
+ # we write the file data
5
+ class ExtraField::Zip64Placeholder < ExtraField::Generic
6
+ HEADER_ID = ['9999'].pack('H*') # this ID is used by other libraries such as .NET's Ionic.zip
7
+ register_map
8
+
9
+ def initialize(_binstr = nil); end
10
+
11
+ def pack_for_local
12
+ "\x00" * 16
13
+ end
14
+ end
15
+ end
@@ -3,30 +3,30 @@ module Zip
3
3
  ID_MAP = {}
4
4
 
5
5
  def initialize(binstr = nil)
6
- binstr and merge(binstr)
6
+ merge(binstr) if binstr
7
7
  end
8
8
 
9
- def extra_field_type_exist(binstr, id, len, i)
9
+ def extra_field_type_exist(binstr, id, len, index)
10
10
  field_name = ID_MAP[id].name
11
- if self.member?(field_name)
12
- self[field_name].merge(binstr[i, len + 4])
11
+ if member?(field_name)
12
+ self[field_name].merge(binstr[index, len + 4])
13
13
  else
14
- field_obj = ID_MAP[id].new(binstr[i, len + 4])
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, i)
19
+ def extra_field_type_unknown(binstr, len, index)
20
20
  create_unknown_item unless self['Unknown']
21
- if !len || len + 4 > binstr[i..-1].bytesize
22
- self['Unknown'] << binstr[i..-1]
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[i, len + 4]
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.unpack('v').first
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
@@ -51,20 +52,29 @@ module Zip
51
52
  end
52
53
 
53
54
  def create(name)
54
- unless field_class = ID_MAP.values.find { |k| k.name == name }
55
- raise ZipError, "Unknown extra field '#{name}'"
55
+ unless (field_class = ID_MAP.values.find { |k| k.name == name })
56
+ raise Error, "Unknown extra field '#{name}'"
56
57
  end
58
+
57
59
  self[name] = field_class.new
58
60
  end
59
61
 
62
+ # place Unknown last, so "extra" data that is missing the proper signature/size
63
+ # does not prevent known fields from being read back in
64
+ def ordered_values
65
+ result = []
66
+ each { |k, v| k == 'Unknown' ? result.push(v) : result.unshift(v) }
67
+ result
68
+ end
69
+
60
70
  def to_local_bin
61
- self.map { |_, v| v.to_local_bin }.join
71
+ ordered_values.map! { |v| v.to_local_bin.force_encoding('BINARY') }.join
62
72
  end
63
73
 
64
- alias :to_s :to_local_bin
74
+ alias to_s to_local_bin
65
75
 
66
76
  def to_c_dir_bin
67
- self.map { |_, v| v.to_c_dir_bin }.join
77
+ ordered_values.map! { |v| v.to_c_dir_bin.force_encoding('BINARY') }.join
68
78
  end
69
79
 
70
80
  def c_dir_size
@@ -75,14 +85,18 @@ module Zip
75
85
  to_local_bin.bytesize
76
86
  end
77
87
 
78
- alias :length :local_size
79
- alias :size :local_size
88
+ alias length local_size
89
+ alias size local_size
80
90
  end
81
91
  end
82
92
 
83
93
  require 'zip/extra_field/generic'
84
94
  require 'zip/extra_field/universal_time'
95
+ require 'zip/extra_field/old_unix'
85
96
  require 'zip/extra_field/unix'
97
+ require 'zip/extra_field/zip64'
98
+ require 'zip/extra_field/zip64_placeholder'
99
+ require 'zip/extra_field/ntfs'
86
100
 
87
101
  # Copyright (C) 2002, 2003 Thomas Sondergaard
88
102
  # rubyzip is free software; you can redistribute it and/or