ziperu 0.1.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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/Cargo.lock +879 -0
  4. data/Cargo.toml +8 -0
  5. data/LICENSE-rubyzip.txt +24 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +276 -0
  8. data/ext/ziperu/Cargo.toml +35 -0
  9. data/ext/ziperu/build.rs +4 -0
  10. data/ext/ziperu/extconf.rb +6 -0
  11. data/ext/ziperu/src/archive.rs +713 -0
  12. data/ext/ziperu/src/errors.rs +143 -0
  13. data/ext/ziperu/src/extract.rs +505 -0
  14. data/ext/ziperu/src/gvl.rs +64 -0
  15. data/ext/ziperu/src/lib.rs +78 -0
  16. data/ext/ziperu/src/reader.rs +351 -0
  17. data/ext/ziperu/src/source.rs +296 -0
  18. data/ext/ziperu/src/writer.rs +916 -0
  19. data/ext/ziperu/src/zipcrypto.rs +126 -0
  20. data/lib/rubyzip.rb +4 -0
  21. data/lib/zip/central_directory.rb +5 -0
  22. data/lib/zip/compressor.rb +5 -0
  23. data/lib/zip/constants.rb +5 -0
  24. data/lib/zip/crypto/aes_encryption.rb +5 -0
  25. data/lib/zip/crypto/decrypted_io.rb +5 -0
  26. data/lib/zip/crypto/encryption.rb +5 -0
  27. data/lib/zip/crypto/null_encryption.rb +5 -0
  28. data/lib/zip/crypto/traditional_encryption.rb +5 -0
  29. data/lib/zip/crypto.rb +5 -0
  30. data/lib/zip/decompressor.rb +5 -0
  31. data/lib/zip/deflater.rb +5 -0
  32. data/lib/zip/dirtyable.rb +5 -0
  33. data/lib/zip/dos_time.rb +5 -0
  34. data/lib/zip/entry.rb +5 -0
  35. data/lib/zip/entry_set.rb +5 -0
  36. data/lib/zip/errors.rb +5 -0
  37. data/lib/zip/extra_field/aes.rb +5 -0
  38. data/lib/zip/extra_field/generic.rb +5 -0
  39. data/lib/zip/extra_field/ntfs.rb +5 -0
  40. data/lib/zip/extra_field/old_unix.rb +5 -0
  41. data/lib/zip/extra_field/universal_time.rb +5 -0
  42. data/lib/zip/extra_field/unix.rb +5 -0
  43. data/lib/zip/extra_field/unknown.rb +5 -0
  44. data/lib/zip/extra_field/zip64.rb +5 -0
  45. data/lib/zip/extra_field.rb +5 -0
  46. data/lib/zip/file.rb +5 -0
  47. data/lib/zip/file_split.rb +5 -0
  48. data/lib/zip/filesystem/dir.rb +5 -0
  49. data/lib/zip/filesystem/directory_iterator.rb +5 -0
  50. data/lib/zip/filesystem/file.rb +5 -0
  51. data/lib/zip/filesystem/file_stat.rb +5 -0
  52. data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
  53. data/lib/zip/filesystem.rb +5 -0
  54. data/lib/zip/inflater.rb +5 -0
  55. data/lib/zip/input_stream.rb +5 -0
  56. data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
  57. data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
  58. data/lib/zip/ioextras/fake_io.rb +5 -0
  59. data/lib/zip/ioextras.rb +5 -0
  60. data/lib/zip/null_compressor.rb +5 -0
  61. data/lib/zip/null_decompressor.rb +5 -0
  62. data/lib/zip/null_input_stream.rb +5 -0
  63. data/lib/zip/output_stream.rb +5 -0
  64. data/lib/zip/pass_thru_compressor.rb +5 -0
  65. data/lib/zip/pass_thru_decompressor.rb +5 -0
  66. data/lib/zip/streamable_directory.rb +5 -0
  67. data/lib/zip/streamable_stream.rb +5 -0
  68. data/lib/zip/version.rb +5 -0
  69. data/lib/zip.rb +19 -0
  70. data/lib/ziperu/central_directory.rb +136 -0
  71. data/lib/ziperu/compressor.rb +14 -0
  72. data/lib/ziperu/constants.rb +148 -0
  73. data/lib/ziperu/crypto.rb +72 -0
  74. data/lib/ziperu/decompressor.rb +35 -0
  75. data/lib/ziperu/deflater.rb +41 -0
  76. data/lib/ziperu/dirtyable.rb +48 -0
  77. data/lib/ziperu/dos_time.rb +87 -0
  78. data/lib/ziperu/entry.rb +760 -0
  79. data/lib/ziperu/entry_set.rb +147 -0
  80. data/lib/ziperu/errors.rb +154 -0
  81. data/lib/ziperu/extra_field/aes.rb +52 -0
  82. data/lib/ziperu/extra_field/generic.rb +45 -0
  83. data/lib/ziperu/extra_field/ntfs.rb +98 -0
  84. data/lib/ziperu/extra_field/old_unix.rb +52 -0
  85. data/lib/ziperu/extra_field/universal_time.rb +81 -0
  86. data/lib/ziperu/extra_field/unix.rb +45 -0
  87. data/lib/ziperu/extra_field/unknown.rb +37 -0
  88. data/lib/ziperu/extra_field/zip64.rb +86 -0
  89. data/lib/ziperu/extra_field.rb +109 -0
  90. data/lib/ziperu/file.rb +614 -0
  91. data/lib/ziperu/file_split.rb +93 -0
  92. data/lib/ziperu/filesystem/dir.rb +88 -0
  93. data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
  94. data/lib/ziperu/filesystem/file.rb +265 -0
  95. data/lib/ziperu/filesystem/file_stat.rb +112 -0
  96. data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
  97. data/lib/ziperu/filesystem.rb +76 -0
  98. data/lib/ziperu/inflater.rb +60 -0
  99. data/lib/ziperu/input_stream.rb +296 -0
  100. data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
  101. data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
  102. data/lib/ziperu/ioextras/fake_io.rb +41 -0
  103. data/lib/ziperu/ioextras.rb +42 -0
  104. data/lib/ziperu/null_compressor.rb +20 -0
  105. data/lib/ziperu/null_decompressor.rb +25 -0
  106. data/lib/ziperu/null_input_stream.rb +55 -0
  107. data/lib/ziperu/output_stream.rb +223 -0
  108. data/lib/ziperu/pass_thru_compressor.rb +29 -0
  109. data/lib/ziperu/pass_thru_decompressor.rb +37 -0
  110. data/lib/ziperu/streamable_directory.rb +15 -0
  111. data/lib/ziperu/streamable_stream.rb +59 -0
  112. data/lib/ziperu/version.rb +7 -0
  113. data/lib/ziperu.rb +116 -0
  114. metadata +174 -0
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+
5
+ module Ziperu
6
+ class EntrySet # :nodoc:all
7
+ include Enumerable
8
+
9
+ attr_reader :entry_set
10
+ protected :entry_set
11
+
12
+ def initialize(an_enumerable = [])
13
+ super()
14
+ # Keyed by name for lookup, as in rubyzip; `@all` keeps every entry
15
+ # in order, which only differs when an archive holds several records
16
+ # under one name (see `push_record`).
17
+ @entry_set = {}
18
+ @all = []
19
+ @shadowed = 0
20
+ an_enumerable.each { |o| push(o) }
21
+ end
22
+
23
+ def include?(entry)
24
+ @entry_set.include?(to_key(entry))
25
+ end
26
+
27
+ def find_entry(entry)
28
+ @entry_set[to_key(entry)]
29
+ end
30
+
31
+ def <<(entry)
32
+ return unless entry
33
+
34
+ key = to_key(entry)
35
+ old = @entry_set[key]
36
+ return if old.equal?(entry)
37
+
38
+ remove_from_all(old) if old
39
+ @entry_set[key] = entry
40
+ @all << entry
41
+ end
42
+
43
+ alias push <<
44
+
45
+ # Adds a record read from an archive. Unlike `<<`, an earlier record
46
+ # of the very same name stays listed (and readable); the lookup by name
47
+ # finds the later one, as rubyzip's does. Names that only share a key
48
+ # (`Ziperu.case_insensitive_match`, or a trailing slash) collapse to
49
+ # the later one, as in rubyzip.
50
+ def push_record(entry)
51
+ key = to_key(entry)
52
+ old = @entry_set[key]
53
+ return self << entry if old.nil? || old.name.b != entry.name.b
54
+
55
+ @shadowed += 1
56
+ @entry_set[key] = entry
57
+ @all << entry
58
+ end
59
+
60
+ # True for a record that a later one of the same name hides from the
61
+ # lookup by name: it is listed and readable, but not written back.
62
+ def shadowed?(entry)
63
+ return false if @shadowed.zero?
64
+
65
+ current = @entry_set[to_key(entry)]
66
+ !current.nil? && !current.equal?(entry)
67
+ end
68
+
69
+ def size
70
+ @all.size
71
+ end
72
+
73
+ alias length size
74
+
75
+ def delete(entry)
76
+ key = to_key(entry)
77
+ removed = @entry_set.delete(key)
78
+ return if removed.nil?
79
+
80
+ remove_from_all(removed)
81
+ # A record this one shadowed takes over its name.
82
+ if @shadowed.positive? && (shadowed = @all.reverse_each.find { |e| to_key(e) == key })
83
+ @shadowed -= 1
84
+ @entry_set[key] = shadowed
85
+ end
86
+ entry
87
+ end
88
+
89
+ def each(&block)
90
+ entries.each(&block)
91
+ end
92
+
93
+ def entries
94
+ sorted_entries
95
+ end
96
+
97
+ # deep clone
98
+ def dup
99
+ copy = EntrySet.new
100
+ @all.each { |e| copy.push_record(e.dup) }
101
+ copy
102
+ end
103
+
104
+ def ==(other)
105
+ return false unless other.kind_of?(EntrySet)
106
+
107
+ entries == other.entries
108
+ end
109
+
110
+ def parent(entry)
111
+ @entry_set[to_key(entry.parent_as_string)]
112
+ end
113
+
114
+ def glob(pattern, flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH | ::File::FNM_EXTGLOB)
115
+ entries.filter_map do |entry|
116
+ next nil unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
117
+
118
+ yield(entry) if block_given?
119
+ entry
120
+ end
121
+ end
122
+
123
+ protected
124
+
125
+ # A copy: callers rename or remove entries while iterating.
126
+ def sorted_entries
127
+ ::Ziperu.sort_entries ? @all.sort_by { |e| to_key(e) } : @all.dup
128
+ end
129
+
130
+ private
131
+
132
+ # Removes `entry` itself (not another of the same name) from `@all`.
133
+ def remove_from_all(entry)
134
+ i = @all.index { |e| e.equal?(entry) }
135
+ @all.delete_at(i) if i
136
+ end
137
+
138
+ # Keyed by name bytes: entry names are binary strings (as in rubyzip),
139
+ # and a lookup with the same bytes in another encoding should find
140
+ # them (which in rubyzip it does not).
141
+ def to_key(entry)
142
+ k = entry.to_s.b.chomp('/')
143
+ k.downcase! if ::Ziperu.case_insensitive_match
144
+ k
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The rubyzip 3.x exception hierarchy, with rubyzip's constructor signatures
4
+ # and messages so rescue clauses and error output are unchanged.
5
+ #
6
+ # The native core looks these up by name, so every class here must exist
7
+ # before the extension raises.
8
+ module Ziperu
9
+ # The superclass for all error types. Simply rescue this one if you don't
10
+ # need to know what sort of error has been raised.
11
+ class Error < StandardError; end
12
+
13
+ # Error raised if an unsupported compression method is used.
14
+ class CompressionMethodError < Error
15
+ attr_reader :compression_method
16
+
17
+ def initialize(method)
18
+ super()
19
+ @compression_method = method
20
+ end
21
+
22
+ def message
23
+ "Unsupported compression method: #{COMPRESSION_METHODS[@compression_method]}."
24
+ end
25
+ end
26
+
27
+ # Error raised if there is a problem while decompressing an archive entry.
28
+ class DecompressionError < Error
29
+ attr_reader :zlib_error
30
+
31
+ def initialize(zlib_error)
32
+ super()
33
+ @zlib_error = zlib_error
34
+ end
35
+
36
+ def message
37
+ "Zlib error ('#{@zlib_error.message}') while inflating."
38
+ end
39
+ end
40
+
41
+ # Error raised when trying to extract an archive entry over an existing file.
42
+ class DestinationExistsError < Error
43
+ def initialize(destination)
44
+ super()
45
+ @destination = destination
46
+ end
47
+
48
+ def message
49
+ "Cannot create file or directory '#{@destination}'. " \
50
+ 'A file already exists with that name.'
51
+ end
52
+ end
53
+
54
+ # Error raised when adding an entry whose name already exists.
55
+ class EntryExistsError < Error
56
+ def initialize(source, name)
57
+ super()
58
+ @source = source
59
+ @name = name
60
+ end
61
+
62
+ def message
63
+ "'#{@source}' failed. Entry #{@name} already exists."
64
+ end
65
+ end
66
+
67
+ # Error raised when an entry name is invalid.
68
+ class EntryNameError < Error
69
+ def initialize(name = nil)
70
+ super()
71
+ @name = name
72
+ end
73
+
74
+ def message
75
+ if @name.nil?
76
+ 'Illegal entry name. Names must have fewer than 65,536 characters.'
77
+ else
78
+ "Illegal entry name '#{@name}'. Names must not start with '/'."
79
+ end
80
+ end
81
+ end
82
+
83
+ # Error raised if an entry is larger on extraction than advertised.
84
+ class EntrySizeError < Error
85
+ attr_reader :entry
86
+
87
+ def initialize(entry)
88
+ super()
89
+ @entry = entry
90
+ end
91
+
92
+ def message
93
+ "Entry '#{@entry.name}' should be #{@entry.size}B, but is larger when inflated."
94
+ end
95
+ end
96
+
97
+ class EntryNumberMismatchError < Error
98
+ def initialize(size, size_in_bytes)
99
+ super()
100
+ @size = size
101
+ @size_in_bytes = size_in_bytes
102
+ end
103
+
104
+ def message
105
+ "Zip consistency problem: an impossibly high number of entries (#{@size}) " \
106
+ 'is declared in this file, compared to the size of the central directory ' \
107
+ "(#{@size_in_bytes} bytes)."
108
+ end
109
+ end
110
+
111
+ # Error raised if a split archive is read.
112
+ class SplitArchiveError < Error
113
+ def message
114
+ 'Rubyzip cannot extract from split archives at this time.'
115
+ end
116
+ end
117
+
118
+ # Error raised if there is not enough metadata for the entry to be streamed.
119
+ class StreamingError < Error
120
+ attr_reader :entry
121
+
122
+ def initialize(entry)
123
+ super()
124
+ @entry = entry
125
+ end
126
+
127
+ def message
128
+ "The local header of this entry ('#{@entry.name}') does not contain " \
129
+ 'the correct metadata for `Ziperu::InputStream` to be able to ' \
130
+ 'uncompress it. Please use `Ziperu::File` instead of `Ziperu::InputStream`.'
131
+ end
132
+ end
133
+
134
+ class GPFBit3Error < Error; end
135
+ class InternalError < Error; end
136
+
137
+ # Raised when an archive exceeds `Ziperu.max_entries` or
138
+ # `Ziperu.max_total_size` on open.
139
+ class LimitError < Error
140
+ def initialize(what, actual, limit)
141
+ super("archive has #{what} of #{actual}, over the limit of #{limit}")
142
+ end
143
+ end
144
+
145
+ PASSWORD_REQUIRED_MESSAGE = 'A password is required to decode this zip file.'
146
+
147
+ # Raised by the Rust core where it lacks the domain object a rubyzip
148
+ # exception wants; the Ruby layer re-raises the rubyzip-shaped error.
149
+ module Native
150
+ class SizeLimitError < Ziperu::Error; end
151
+ class PasswordError < Ziperu::Error; end
152
+ class UnsupportedError < Ziperu::Error; end
153
+ end
154
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+
5
+ module Ziperu
6
+ # Info-ZIP Extra for AES encryption
7
+ class ExtraField::AES < ExtraField::Generic # :nodoc:
8
+ attr_accessor :vendor_version, :vendor_id, :encryption_strength, :compression_method
9
+
10
+ HEADER_ID = [0x9901].pack('v')
11
+ register_map
12
+
13
+ def initialize(binstr = nil)
14
+ @vendor_version = nil
15
+ @vendor_id = nil
16
+ @encryption_strength = nil
17
+ @compression_method = nil
18
+ binstr && merge(binstr)
19
+ end
20
+
21
+ def ==(other)
22
+ @vendor_version == other.vendor_version &&
23
+ @vendor_id == other.vendor_id &&
24
+ @encryption_strength == other.encryption_strength &&
25
+ @compression_method == other.compression_method
26
+ end
27
+
28
+ def merge(binstr)
29
+ return if binstr.empty?
30
+
31
+ size, content = initial_parse(binstr)
32
+ (size && content) || return
33
+
34
+ @vendor_version, @vendor_id,
35
+ @encryption_strength, @compression_method = content.unpack('va2Cv')
36
+ end
37
+
38
+ # We can never suppress the AES extra field as it is needed to read the file.
39
+ def suppress?
40
+ false
41
+ end
42
+
43
+ def pack_for_local
44
+ [@vendor_version, @vendor_id,
45
+ @encryption_strength, @compression_method].pack('va2Cv')
46
+ end
47
+
48
+ def pack_for_c_dir
49
+ pack_for_local
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+
5
+ module Ziperu
6
+ class ExtraField::Generic # :nodoc:
7
+ def self.register_map
8
+ return unless const_defined?(:HEADER_ID)
9
+
10
+ ::Ziperu::ExtraField::ID_MAP[const_get(:HEADER_ID)] = self
11
+ end
12
+
13
+ def self.name
14
+ @name ||= to_s.split('::').last.downcase.to_sym
15
+ end
16
+
17
+ # return field [size, content] or false
18
+ def initial_parse(binstr)
19
+ return false unless binstr
20
+
21
+ if binstr[0, 2] != self.class.const_get(:HEADER_ID)
22
+ warn 'WARNING: weird extra field header ID. Skip parsing it.'
23
+ return false
24
+ end
25
+
26
+ [binstr[2, 2].unpack1('v'), binstr[4..]]
27
+ end
28
+
29
+ # Default strategy is to suppress all extra fields if we're asked to.
30
+ # Specific extra field types can override this if they need to be kept.
31
+ def suppress?
32
+ true
33
+ end
34
+
35
+ def to_local_bin
36
+ s = pack_for_local
37
+ (self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
38
+ end
39
+
40
+ def to_c_dir_bin
41
+ s = pack_for_c_dir
42
+ (self.class.const_get(:HEADER_ID) + [s.bytesize].pack('v')) << s
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+
5
+ module Ziperu
6
+ # PKWARE NTFS Extra Field (0x000a)
7
+ # Only Tag 0x0001 is supported
8
+ class ExtraField::NTFS < ExtraField::Generic # :nodoc:
9
+ HEADER_ID = [0x000A].pack('v')
10
+ register_map
11
+
12
+ WINDOWS_TICK = 10_000_000.0
13
+ SEC_TO_UNIX_EPOCH = 11_644_473_600
14
+
15
+ def initialize(binstr = nil)
16
+ @ctime = nil
17
+ @mtime = nil
18
+ @atime = nil
19
+ binstr && merge(binstr)
20
+ end
21
+
22
+ attr_accessor :atime, :ctime, :mtime
23
+
24
+ def merge(binstr)
25
+ return if binstr.empty?
26
+
27
+ size, content = initial_parse(binstr)
28
+ (size && content) || return
29
+
30
+ content = content[4..]
31
+ tags = parse_tags(content)
32
+
33
+ tag1 = tags[1]
34
+ return unless tag1
35
+
36
+ ntfs_mtime, ntfs_atime, ntfs_ctime = tag1.unpack('Q<Q<Q<')
37
+ ntfs_mtime && @mtime ||= from_ntfs_time(ntfs_mtime)
38
+ ntfs_atime && @atime ||= from_ntfs_time(ntfs_atime)
39
+ ntfs_ctime && @ctime ||= from_ntfs_time(ntfs_ctime)
40
+ end
41
+
42
+ def ==(other)
43
+ @mtime == other.mtime &&
44
+ @atime == other.atime &&
45
+ @ctime == other.ctime
46
+ end
47
+
48
+ # Info-ZIP note states this extra field is stored at local header
49
+ def pack_for_local
50
+ pack_for_c_dir
51
+ end
52
+
53
+ # But 7-zip for Windows only stores at central dir
54
+ def pack_for_c_dir
55
+ # reserved 0 and tag 1
56
+ s = [0, 1].pack('Vv')
57
+
58
+ tag1 = (+'').force_encoding(Encoding::BINARY)
59
+ if @mtime
60
+ tag1 << [to_ntfs_time(@mtime)].pack('Q<')
61
+ if @atime
62
+ tag1 << [to_ntfs_time(@atime)].pack('Q<')
63
+ tag1 << [to_ntfs_time(@ctime)].pack('Q<') if @ctime
64
+ end
65
+ end
66
+ s << [tag1.bytesize].pack('v') << tag1
67
+ s
68
+ end
69
+
70
+ private
71
+
72
+ def parse_tags(content)
73
+ return {} if content.nil?
74
+
75
+ tags = {}
76
+ i = 0
77
+ while i < content.bytesize
78
+ tag, size = content[i, 4].unpack('vv')
79
+ i += 4
80
+ break unless tag && size
81
+
82
+ value = content[i, size]
83
+ i += size
84
+ tags[tag] = value
85
+ end
86
+
87
+ tags
88
+ end
89
+
90
+ def from_ntfs_time(ntfs_time)
91
+ ::Ziperu::DOSTime.at((ntfs_time / WINDOWS_TICK) - SEC_TO_UNIX_EPOCH)
92
+ end
93
+
94
+ def to_ntfs_time(time)
95
+ ((time.to_f + SEC_TO_UNIX_EPOCH) * WINDOWS_TICK).to_i
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+
5
+ module Ziperu
6
+ # Olf Info-ZIP Extra for UNIX uid/gid and file timestampes
7
+ class ExtraField::OldUnix < ExtraField::Generic # :nodoc:
8
+ HEADER_ID = 'UX'
9
+ register_map
10
+
11
+ def initialize(binstr = nil)
12
+ @uid = nil
13
+ @gid = nil
14
+ @atime = nil
15
+ @mtime = nil
16
+ binstr && merge(binstr)
17
+ end
18
+
19
+ attr_accessor :uid, :gid, :atime, :mtime
20
+
21
+ def merge(binstr)
22
+ return if binstr.empty?
23
+
24
+ size, content = initial_parse(binstr)
25
+ # size: 0 for central directory. 4 for local header
26
+ return if !size || size == 0
27
+
28
+ atime, mtime, uid, gid = content.unpack('VVvv')
29
+ @uid ||= uid
30
+ @gid ||= gid
31
+ @atime ||= atime
32
+ @mtime ||= mtime # rubocop:disable Naming/MemoizedInstanceVariableName
33
+ end
34
+
35
+ def ==(other)
36
+ @uid == other.uid &&
37
+ @gid == other.gid &&
38
+ @atime == other.atime &&
39
+ @mtime == other.mtime
40
+ end
41
+
42
+ # Fields are nil when only the central directory's (empty) form was
43
+ # seen; they pack as zero rather than failing.
44
+ def pack_for_local
45
+ [@atime, @mtime, @uid, @gid].map(&:to_i).pack('VVvv')
46
+ end
47
+
48
+ def pack_for_c_dir
49
+ [@atime, @mtime].map(&:to_i).pack('VV')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+
5
+ module Ziperu
6
+ # Info-ZIP Additional timestamp field
7
+ class ExtraField::UniversalTime < ExtraField::Generic # :nodoc:
8
+ HEADER_ID = 'UT'
9
+ register_map
10
+
11
+ ATIME_MASK = 0b010
12
+ CTIME_MASK = 0b100
13
+ MTIME_MASK = 0b001
14
+
15
+ def initialize(binstr = nil)
16
+ @ctime = nil
17
+ @mtime = nil
18
+ @atime = nil
19
+ @flag = 0
20
+
21
+ merge(binstr) unless binstr.nil?
22
+ end
23
+
24
+ attr_reader :atime, :ctime, :mtime, :flag
25
+
26
+ def atime=(time)
27
+ @flag = time.nil? ? @flag & ~ATIME_MASK : @flag | ATIME_MASK
28
+ @atime = time
29
+ end
30
+
31
+ def ctime=(time)
32
+ @flag = time.nil? ? @flag & ~CTIME_MASK : @flag | CTIME_MASK
33
+ @ctime = time
34
+ end
35
+
36
+ def mtime=(time)
37
+ @flag = time.nil? ? @flag & ~MTIME_MASK : @flag | MTIME_MASK
38
+ @mtime = time
39
+ end
40
+
41
+ def merge(binstr)
42
+ return if binstr.empty?
43
+
44
+ size, content = initial_parse(binstr)
45
+ return if !size || size <= 0
46
+
47
+ @flag, *times = content.unpack('Cl<l<l<')
48
+
49
+ # Parse the timestamps, in order, based on which flags are set.
50
+ return if times[0].nil?
51
+
52
+ @mtime ||= ::Ziperu::DOSTime.at(times.shift) unless @flag & MTIME_MASK == 0
53
+ return if times[0].nil?
54
+
55
+ @atime ||= ::Ziperu::DOSTime.at(times.shift) unless @flag & ATIME_MASK == 0
56
+ return if times[0].nil?
57
+
58
+ @ctime ||= ::Ziperu::DOSTime.at(times.shift) unless @flag & CTIME_MASK == 0
59
+ end
60
+
61
+ def ==(other)
62
+ @mtime == other.mtime &&
63
+ @atime == other.atime &&
64
+ @ctime == other.ctime
65
+ end
66
+
67
+ def pack_for_local
68
+ s = [@flag].pack('C')
69
+ s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
70
+ s << [@atime.to_i].pack('l<') unless @flag & ATIME_MASK == 0
71
+ s << [@ctime.to_i].pack('l<') unless @flag & CTIME_MASK == 0
72
+ s
73
+ end
74
+
75
+ def pack_for_c_dir
76
+ s = [@flag].pack('C')
77
+ s << [@mtime.to_i].pack('l<') unless @flag & MTIME_MASK == 0
78
+ s
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+
5
+ module Ziperu
6
+ # Info-ZIP Extra for UNIX uid/gid
7
+ class ExtraField::IUnix < ExtraField::Generic # :nodoc:
8
+ HEADER_ID = 'Ux'
9
+ register_map
10
+
11
+ def initialize(binstr = nil)
12
+ @uid = nil
13
+ @gid = nil
14
+ binstr && merge(binstr)
15
+ end
16
+
17
+ attr_accessor :uid, :gid
18
+
19
+ def merge(binstr)
20
+ return if binstr.empty?
21
+
22
+ size, content = initial_parse(binstr)
23
+ # size: 0 for central directory. 4 for local header
24
+ return if !size || size == 0
25
+
26
+ uid, gid = content.unpack('vv')
27
+ @uid = uid
28
+ @gid = gid
29
+ end
30
+
31
+ def ==(other)
32
+ @uid == other.uid && @gid == other.gid
33
+ end
34
+
35
+ # Nil when only the central directory's (empty) form was seen; packed
36
+ # as zero rather than failing.
37
+ def pack_for_local
38
+ [@uid, @gid].map(&:to_i).pack('vv')
39
+ end
40
+
41
+ def pack_for_c_dir
42
+ ''
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+
5
+ require_relative 'generic'
6
+
7
+ module Ziperu
8
+ # A class to hold unknown extra fields so that they are preserved.
9
+ class ExtraField::Unknown < ExtraField::Generic # :nodoc:
10
+ def initialize
11
+ @local_bin = +''
12
+ @cdir_bin = +''
13
+ end
14
+
15
+ def merge(binstr, local: false)
16
+ return if binstr.empty?
17
+
18
+ if local
19
+ @local_bin << binstr
20
+ else
21
+ @cdir_bin << binstr
22
+ end
23
+ end
24
+
25
+ def to_local_bin
26
+ @local_bin
27
+ end
28
+
29
+ def to_c_dir_bin
30
+ @cdir_bin
31
+ end
32
+
33
+ def ==(other)
34
+ @local_bin == other.to_local_bin && @cdir_bin == other.to_c_dir_bin
35
+ end
36
+ end
37
+ end