zip-merge 7.0.0 → 7.1.3

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.
data/lib/zip/merge.rb CHANGED
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "stringio"
4
- require "zlib"
5
- require "tree_haver"
6
- require_relative "merge/version"
3
+ require 'stringio'
4
+ require 'zlib'
5
+ require 'tree_haver'
6
+ require_relative 'merge/version'
7
7
 
8
8
  module Zip
9
9
  module Merge
10
10
  LOCAL = 0x04034b50
11
11
  CENTRAL = 0x02014b50
12
12
  EOCD = 0x06054b50
13
- DOS_EPOCH = [0, 0, 0x21, 0].pack("C*")
13
+ DOS_EPOCH = [0, 0, 0x21, 0].pack('C*')
14
14
 
15
15
  RenderError = Class.new(StandardError) do
16
16
  attr_reader :diagnostic
@@ -32,19 +32,21 @@ module Zip
32
32
  TreeHaver::ZipArchiveEntry.new(
33
33
  path: name,
34
34
  normalized_path: normalize_zip_path(name),
35
- directory: name.end_with?("/"),
35
+ directory: name.end_with?('/'),
36
36
  compression: compression_name(record[:method]),
37
37
  compressed_size: record[:compressed_size],
38
38
  uncompressed_size: record[:uncompressed_size],
39
- crc32: "%08x" % record[:crc32],
39
+ crc32: '%08x' % record[:crc32],
40
40
  local_header_range: TreeHaver::ByteRange.new(start_byte: record[:local_offset], end_byte: local[:data_start]),
41
- data_range: TreeHaver::ByteRange.new(start_byte: local[:data_start], end_byte: local[:data_start] + record[:compressed_size]),
41
+ data_range: TreeHaver::ByteRange.new(start_byte: local[:data_start],
42
+ end_byte: local[:data_start] + record[:compressed_size]),
42
43
  central_directory_range: record[:range]
43
44
  )
44
45
  end.sort_by { |entry| entry.local_header_range.start_byte }
45
46
 
46
47
  TreeHaver::ZipFamilyReport.new(
47
- archive: TreeHaver::ZipArchiveInfo.new(format: "zip", schema: "zip.ksy", entry_count: entries.length, central_directory_range: central[:range]),
48
+ archive: TreeHaver::ZipArchiveInfo.new(format: 'zip', schema: 'zip.ksy', entry_count: entries.length,
49
+ central_directory_range: central[:range]),
48
50
  entries: entries,
49
51
  member_decisions: [],
50
52
  unsafe_entries: unsafe_entries(entries, central[:records]),
@@ -71,59 +73,70 @@ module Zip
71
73
  incoming_entry = incoming_entries[path]
72
74
  if unsafe_by_path[path]
73
75
  unsafe = unsafe_by_path[path]
74
- report.member_decisions << TreeHaver::ZipMemberDecision.new(normalized_path: path, operation: "reject", disposition: "unsafe", reason: unsafe.reason)
76
+ report.member_decisions << TreeHaver::ZipMemberDecision.new(normalized_path: path, operation: 'reject',
77
+ disposition: 'unsafe', reason: unsafe.reason)
75
78
  report.merge_report.diagnostics << diagnostic(unsafe.category, schema_path(path), unsafe.reason)
76
79
  elsif current_entry.nil? && incoming_entry
77
- decision(report, path, "add", "requires_renderer", "member exists only in incoming archive")
80
+ decision(report, path, 'add', 'requires_renderer', 'member exists only in incoming archive')
78
81
  elsif current_entry && incoming_entry.nil?
79
- decision(report, path, "delete", "requires_renderer", "member was removed from incoming archive")
80
- elsif ancestor_entry && same_entry?(current_entry, ancestor_entry) && same_entry?(incoming_entry, ancestor_entry)
81
- report.member_decisions << TreeHaver::ZipMemberDecision.new(normalized_path: path, operation: "preserve", disposition: "safe", reason: "member is unchanged from ancestor")
82
+ decision(report, path, 'delete', 'requires_renderer', 'member was removed from incoming archive')
83
+ elsif ancestor_entry && same_entry?(current_entry,
84
+ ancestor_entry) && same_entry?(incoming_entry, ancestor_entry)
85
+ report.member_decisions << TreeHaver::ZipMemberDecision.new(normalized_path: path, operation: 'preserve',
86
+ disposition: 'safe', reason: 'member is unchanged from ancestor')
82
87
  report.merge_report.preserved_ranges.concat([current_entry.local_header_range, current_entry.data_range])
83
88
  elsif (family = nested_family(path))
84
- report.member_decisions << TreeHaver::ZipMemberDecision.new(normalized_path: path, operation: "delegate", disposition: "requires_renderer", nested_family: family, reason: "structured member can be merged by a nested family before ZIP rendering")
85
- report.merge_report.nested_dispatches << TreeHaver::BinaryNestedDispatch.new(schema_path: "#{schema_path(path)}/data", family: family, status: "planned")
89
+ report.member_decisions << TreeHaver::ZipMemberDecision.new(normalized_path: path, operation: 'delegate',
90
+ disposition: 'requires_renderer', nested_family: family, reason: 'structured member can be merged by a nested family before ZIP rendering')
91
+ report.merge_report.nested_dispatches << TreeHaver::BinaryNestedDispatch.new(
92
+ schema_path: "#{schema_path(path)}/data", family: family, status: 'planned'
93
+ )
86
94
  report.merge_report.rewritten_nodes << schema_path(path)
87
95
  report.merge_report.checksum_updates << "#{schema_path(path)}/crc32"
88
96
  else
89
- decision(report, path, "rewrite", "requires_renderer", "member bytes or metadata changed")
97
+ decision(report, path, 'rewrite', 'requires_renderer', 'member bytes or metadata changed')
90
98
  report.merge_report.checksum_updates << "#{schema_path(path)}/crc32"
91
99
  end
92
100
  report.merge_report.matched_schema_paths << schema_path(path)
93
101
  end
94
102
  unless report.merge_report.rewritten_nodes.empty? && report.merge_report.checksum_updates.empty?
95
- report.merge_report.rewritten_nodes << "/central_directory"
96
- report.merge_report.checksum_updates.concat(["/central_directory/size", "/central_directory/offset"])
103
+ report.merge_report.rewritten_nodes << '/central_directory'
104
+ report.merge_report.checksum_updates.concat(['/central_directory/size', '/central_directory/offset'])
97
105
  end
98
106
  report
99
107
  end
100
108
 
101
109
  def render_with_raw_preservation(source:, plan:, member_bytes: {}, compression: 0)
102
- raise render_error("unsupported_compression", "/render/options/compression", "unsupported raw-preserving compression method") unless [0, 8].include?(compression)
110
+ unless [
111
+ 0, 8
112
+ ].include?(compression)
113
+ raise render_error('unsupported_compression', '/render/options/compression',
114
+ 'unsupported raw-preserving compression method')
115
+ end
103
116
 
104
117
  source = source.b
105
118
  source_inventory = parse_zip_inventory(source)
106
119
  central = scan_central_directory(source)
107
120
  source_entries = entries_by_path(source_inventory.entries)
108
121
  raw_ranges = raw_local_record_ranges(source, source_entries)
109
- output = +"".b
122
+ output = +''.b
110
123
  central_records = []
111
124
  entries = entries_by_path(plan.entries)
112
125
  plan.member_decisions.each do |member|
113
126
  entry = entries[member.normalized_path]
114
127
  case member.operation
115
- when "reject"
116
- raise render_error("rejected_member", schema_path(member.normalized_path), member.reason)
117
- when "delete"
128
+ when 'reject'
129
+ raise render_error('rejected_member', schema_path(member.normalized_path), member.reason)
130
+ when 'delete'
118
131
  next
119
- when "preserve"
132
+ when 'preserve'
120
133
  source_entry = source_entries.fetch(member.normalized_path)
121
134
  validate_raw_preserve_entry!(source, central, source_entry)
122
135
  range = raw_ranges.fetch(member.normalized_path)
123
136
  offset = output.bytesize
124
137
  output << source.byteslice(range.start_byte...range.end_byte)
125
138
  central_records << central_record_from_entry(source_entry, offset)
126
- when "add", "rewrite", "delegate"
139
+ when 'add', 'rewrite', 'delegate'
127
140
  content = member_bytes.fetch(member.normalized_path)
128
141
  rendered, record = rendered_local_record(entry, content.b, compression, output.bytesize)
129
142
  output << rendered
@@ -138,12 +151,14 @@ module Zip
138
151
  output << eocd_record(central_records.length, central_size, central_start)
139
152
  report = parse_zip_inventory(output)
140
153
  merge_report = plan.merge_report
141
- merge_report.preserved_ranges = plan.member_decisions.filter_map { |member| raw_ranges[member.normalized_path] if member.operation == "preserve" }
154
+ merge_report.preserved_ranges = plan.member_decisions.filter_map do |member|
155
+ raw_ranges[member.normalized_path] if member.operation == 'preserve'
156
+ end
142
157
  [output, report, merge_report]
143
158
  end
144
159
 
145
160
  def new_stored_zip(entries)
146
- output = +"".b
161
+ output = +''.b
147
162
  central = []
148
163
  entries.keys.sort.each do |name|
149
164
  rendered, record = rendered_local_record(path_entry(name, entries[name]), entries[name].b, 0, output.bytesize)
@@ -157,34 +172,38 @@ module Zip
157
172
  end
158
173
 
159
174
  def empty_report
160
- TreeHaver::BinaryMergeReport.new(format: "zip", schema: "zip.ksy", matched_schema_paths: [], preserved_ranges: [], rewritten_nodes: [], checksum_updates: [], nested_dispatches: [], diagnostics: [])
175
+ Binary::Merge.preservation_report(format: 'zip', schema: 'zip.ksy', matched_schema_paths: [],
176
+ preserved_ranges: [])
161
177
  end
162
178
 
163
179
  def scan_central_directory(source)
164
180
  eocd = source.bytesize - 22
165
- eocd -= 1 while eocd >= 0 && source.byteslice(eocd, 4).unpack1("V") != EOCD
166
- raise "missing ZIP end of central directory" if eocd.negative?
167
- size = source.byteslice(eocd + 12, 4).unpack1("V")
168
- offset = source.byteslice(eocd + 16, 4).unpack1("V")
169
- comment_length = source.byteslice(eocd + 20, 2).unpack1("v")
181
+ eocd -= 1 while eocd >= 0 && source.byteslice(eocd, 4).unpack1('V') != EOCD
182
+ raise 'missing ZIP end of central directory' if eocd.negative?
183
+
184
+ size = source.byteslice(eocd + 12, 4).unpack1('V')
185
+ offset = source.byteslice(eocd + 16, 4).unpack1('V')
186
+ comment_length = source.byteslice(eocd + 20, 2).unpack1('v')
170
187
  records = {}
171
188
  cursor = offset
172
189
  while cursor < offset + size
173
- raise "unexpected central directory record" unless source.byteslice(cursor, 4).unpack1("V") == CENTRAL
174
- name_len = source.byteslice(cursor + 28, 2).unpack1("v")
175
- extra_len = source.byteslice(cursor + 30, 2).unpack1("v")
176
- comment_len = source.byteslice(cursor + 32, 2).unpack1("v")
190
+ raise 'unexpected central directory record' unless source.byteslice(cursor, 4).unpack1('V') == CENTRAL
191
+
192
+ name_len = source.byteslice(cursor + 28, 2).unpack1('v')
193
+ extra_len = source.byteslice(cursor + 30, 2).unpack1('v')
194
+ comment_len = source.byteslice(cursor + 32, 2).unpack1('v')
177
195
  name = source.byteslice(cursor + 46, name_len)
178
196
  records[name] = {
179
- range: TreeHaver::ByteRange.new(start_byte: cursor, end_byte: cursor + 46 + name_len + extra_len + comment_len),
180
- flags: source.byteslice(cursor + 8, 2).unpack1("v"),
181
- method: source.byteslice(cursor + 10, 2).unpack1("v"),
182
- crc32: source.byteslice(cursor + 16, 4).unpack1("V"),
183
- compressed_size: source.byteslice(cursor + 20, 4).unpack1("V"),
184
- uncompressed_size: source.byteslice(cursor + 24, 4).unpack1("V"),
197
+ range: TreeHaver::ByteRange.new(start_byte: cursor,
198
+ end_byte: cursor + 46 + name_len + extra_len + comment_len),
199
+ flags: source.byteslice(cursor + 8, 2).unpack1('v'),
200
+ method: source.byteslice(cursor + 10, 2).unpack1('v'),
201
+ crc32: source.byteslice(cursor + 16, 4).unpack1('V'),
202
+ compressed_size: source.byteslice(cursor + 20, 4).unpack1('V'),
203
+ uncompressed_size: source.byteslice(cursor + 24, 4).unpack1('V'),
185
204
  extra_length: extra_len,
186
205
  comment_length: comment_len,
187
- local_offset: source.byteslice(cursor + 42, 4).unpack1("V")
206
+ local_offset: source.byteslice(cursor + 42, 4).unpack1('V')
188
207
  }
189
208
  cursor = records[name][:range].end_byte
190
209
  end
@@ -194,32 +213,67 @@ module Zip
194
213
  def scan_local_headers(source, records)
195
214
  records.transform_values do |record|
196
215
  cursor = record[:local_offset]
197
- raise "unexpected ZIP local header" unless source.byteslice(cursor, 4).unpack1("V") == LOCAL
198
- name_len = source.byteslice(cursor + 26, 2).unpack1("v")
199
- extra_len = source.byteslice(cursor + 28, 2).unpack1("v")
216
+ raise 'unexpected ZIP local header' unless source.byteslice(cursor, 4).unpack1('V') == LOCAL
217
+
218
+ name_len = source.byteslice(cursor + 26, 2).unpack1('v')
219
+ extra_len = source.byteslice(cursor + 28, 2).unpack1('v')
200
220
  { data_start: cursor + 30 + name_len + extra_len, extra_length: extra_len }
201
221
  end
202
222
  end
203
223
 
204
224
  def validate_raw_preserve_entry!(source, central, entry)
205
- raise render_error("archive_comment", "/archive/comment", "raw-preserving ZIP renderer does not yet preserve archive comments") if central[:archive_comment]
225
+ if central[:archive_comment]
226
+ raise render_error('archive_comment', '/archive/comment',
227
+ 'raw-preserving ZIP renderer does not yet preserve archive comments')
228
+ end
229
+
206
230
  record = central[:records].fetch(entry.path)
207
- raise render_error("encrypted_member", schema_path(entry.normalized_path), "raw-preserving ZIP renderer rejects encrypted member #{entry.normalized_path}") unless (record[:flags] & 0x1).zero?
208
- raise render_error("unsupported_compression", schema_path(entry.normalized_path), "raw-preserving ZIP renderer rejects unsupported compression #{entry.compression.inspect}") unless [0, 8].include?(record[:method])
209
- raise render_error("central_directory_extra_field", schema_path(entry.normalized_path), "raw-preserving ZIP renderer does not yet preserve central-directory extra fields for #{entry.normalized_path}") unless record[:extra_length].zero?
210
- raise render_error("member_comment", schema_path(entry.normalized_path), "raw-preserving ZIP renderer does not yet preserve member comments for #{entry.normalized_path}") unless record[:comment_length].zero?
211
- local_extra = source.byteslice(entry.local_header_range.start_byte + 28, 2).unpack1("v")
212
- raise render_error("local_header_extra_field", schema_path(entry.normalized_path), "raw-preserving ZIP renderer does not yet preserve local extra fields for #{entry.normalized_path}") unless local_extra.zero?
231
+ unless (record[:flags] & 0x1).zero?
232
+ raise render_error('encrypted_member', schema_path(entry.normalized_path),
233
+ "raw-preserving ZIP renderer rejects encrypted member #{entry.normalized_path}")
234
+ end
235
+ unless [
236
+ 0, 8
237
+ ].include?(record[:method])
238
+ raise render_error('unsupported_compression', schema_path(entry.normalized_path),
239
+ "raw-preserving ZIP renderer rejects unsupported compression #{entry.compression.inspect}")
240
+ end
241
+ unless record[:extra_length].zero?
242
+ raise render_error('central_directory_extra_field', schema_path(entry.normalized_path),
243
+ "raw-preserving ZIP renderer does not yet preserve central-directory extra fields for #{entry.normalized_path}")
244
+ end
245
+ unless record[:comment_length].zero?
246
+ raise render_error('member_comment', schema_path(entry.normalized_path),
247
+ "raw-preserving ZIP renderer does not yet preserve member comments for #{entry.normalized_path}")
248
+ end
249
+
250
+ local_extra = source.byteslice(entry.local_header_range.start_byte + 28, 2).unpack1('v')
251
+ return if local_extra.zero?
252
+
253
+ raise render_error('local_header_extra_field', schema_path(entry.normalized_path),
254
+ "raw-preserving ZIP renderer does not yet preserve local extra fields for #{entry.normalized_path}")
213
255
  end
214
256
 
215
257
  def unsafe_entries(entries, records)
216
258
  seen = {}
217
259
  entries.flat_map do |entry|
218
260
  list = []
219
- list << TreeHaver::ZipUnsafeEntry.new(path: entry.path, normalized_path: entry.normalized_path, category: "path_traversal", reason: "entry escapes the archive root") if escapes_root?(entry.path)
220
- list << TreeHaver::ZipUnsafeEntry.new(path: entry.path, normalized_path: entry.normalized_path, category: "duplicate_normalized_path", reason: "normalized path collides with an existing entry") if seen[entry.normalized_path] && seen[entry.normalized_path] != entry.path
221
- list << TreeHaver::ZipUnsafeEntry.new(path: entry.path, normalized_path: entry.normalized_path, category: "encrypted_member", reason: "encrypted member cannot be rendered by the default provider") unless (records[entry.path][:flags] & 0x1).zero?
222
- list << TreeHaver::ZipUnsafeEntry.new(path: entry.path, normalized_path: entry.normalized_path, category: "signing_sensitive_member", reason: "signature-bearing member mutation is not enabled") if signing_sensitive?(entry.normalized_path)
261
+ if escapes_root?(entry.path)
262
+ list << TreeHaver::ZipUnsafeEntry.new(path: entry.path, normalized_path: entry.normalized_path,
263
+ category: 'path_traversal', reason: 'entry escapes the archive root')
264
+ end
265
+ if seen[entry.normalized_path] && seen[entry.normalized_path] != entry.path
266
+ list << TreeHaver::ZipUnsafeEntry.new(path: entry.path, normalized_path: entry.normalized_path,
267
+ category: 'duplicate_normalized_path', reason: 'normalized path collides with an existing entry')
268
+ end
269
+ unless (records[entry.path][:flags] & 0x1).zero?
270
+ list << TreeHaver::ZipUnsafeEntry.new(path: entry.path, normalized_path: entry.normalized_path,
271
+ category: 'encrypted_member', reason: 'encrypted member cannot be rendered by the default provider')
272
+ end
273
+ if signing_sensitive?(entry.normalized_path)
274
+ list << TreeHaver::ZipUnsafeEntry.new(path: entry.path, normalized_path: entry.normalized_path,
275
+ category: 'signing_sensitive_member', reason: 'signature-bearing member mutation is not enabled')
276
+ end
223
277
  seen[entry.normalized_path] = entry.path
224
278
  list
225
279
  end
@@ -228,57 +282,79 @@ module Zip
228
282
  def rendered_local_record(entry, content, method, offset)
229
283
  payload = method == 8 ? Zlib::Deflate.deflate(content) : content
230
284
  crc = Zlib.crc32(content)
231
- header = [LOCAL, 20, 0, method].pack("Vvvv") + DOS_EPOCH + [crc, payload.bytesize, content.bytesize, entry.path.bytesize, 0].pack("VVVvv") + entry.path
232
- [header + payload, { name: entry.path, method: method, crc32: crc, compressed_size: payload.bytesize, uncompressed_size: content.bytesize, offset: offset, flags: 0 }]
285
+ header = [LOCAL, 20, 0,
286
+ method].pack('Vvvv') + DOS_EPOCH + [crc, payload.bytesize, content.bytesize, entry.path.bytesize,
287
+ 0].pack('VVVvv') + entry.path
288
+ [header + payload,
289
+ { name: entry.path, method: method, crc32: crc, compressed_size: payload.bytesize, uncompressed_size: content.bytesize,
290
+ offset: offset, flags: 0 }]
233
291
  end
234
292
 
235
293
  def central_directory_record(record)
236
- [CENTRAL, 20, 20, record[:flags], record[:method]].pack("Vvvvv") + DOS_EPOCH + [record[:crc32], record[:compressed_size], record[:uncompressed_size], record[:name].bytesize, 0, 0, 0, 0, 0, record[:offset]].pack("VVVvvvvvVV") + record[:name]
294
+ [CENTRAL, 20, 20, record[:flags],
295
+ record[:method]].pack('Vvvvv') + DOS_EPOCH + [record[:crc32], record[:compressed_size], record[:uncompressed_size],
296
+ record[:name].bytesize, 0, 0, 0, 0, 0, record[:offset]].pack('VVVvvvvvVV') + record[:name]
237
297
  end
238
298
 
239
299
  def eocd_record(entries, size, offset)
240
- [EOCD, 0, 0, entries, entries, size, offset, 0].pack("VvvvvVVv")
300
+ [EOCD, 0, 0, entries, entries, size, offset, 0].pack('VvvvvVVv')
241
301
  end
242
302
 
243
- def raw_local_record_ranges(source, entries)
303
+ def raw_local_record_ranges(_source, entries)
244
304
  ordered = entries.values.sort_by { |entry| entry.local_header_range.start_byte }
245
305
  ordered.each_with_index.to_h do |entry, index|
246
306
  end_byte = ordered[index + 1]&.local_header_range&.start_byte || entry.central_directory_range.start_byte
247
- [entry.normalized_path, TreeHaver::ByteRange.new(start_byte: entry.local_header_range.start_byte, end_byte: end_byte)]
307
+ [entry.normalized_path,
308
+ TreeHaver::ByteRange.new(start_byte: entry.local_header_range.start_byte, end_byte: end_byte)]
248
309
  end
249
310
  end
250
311
 
251
312
  def central_record_from_entry(entry, offset)
252
- { name: entry.path, method: entry.compression == "deflate" ? 8 : 0, crc32: entry.crc32.to_i(16), compressed_size: entry.compressed_size, uncompressed_size: entry.uncompressed_size, offset: offset, flags: 0 }
313
+ { name: entry.path, method: entry.compression == 'deflate' ? 8 : 0, crc32: entry.crc32.to_i(16),
314
+ compressed_size: entry.compressed_size, uncompressed_size: entry.uncompressed_size, offset: offset, flags: 0 }
253
315
  end
254
316
 
255
317
  def path_entry(name, content)
256
- TreeHaver::ZipArchiveEntry.new(path: name, normalized_path: normalize_zip_path(name), directory: name.end_with?("/"), compression: "stored", compressed_size: content.bytesize, uncompressed_size: content.bytesize, crc32: "%08x" % Zlib.crc32(content), local_header_range: TreeHaver::ByteRange.new(start_byte: 0, end_byte: 0), data_range: TreeHaver::ByteRange.new(start_byte: 0, end_byte: 0), central_directory_range: TreeHaver::ByteRange.new(start_byte: 0, end_byte: 0))
318
+ TreeHaver::ZipArchiveEntry.new(path: name, normalized_path: normalize_zip_path(name),
319
+ directory: name.end_with?('/'), compression: 'stored', compressed_size: content.bytesize, uncompressed_size: content.bytesize, crc32: '%08x' % Zlib.crc32(content), local_header_range: TreeHaver::ByteRange.new(start_byte: 0, end_byte: 0), data_range: TreeHaver::ByteRange.new(start_byte: 0, end_byte: 0), central_directory_range: TreeHaver::ByteRange.new(start_byte: 0, end_byte: 0))
257
320
  end
258
321
 
259
322
  def decision(report, path, operation, disposition, reason)
260
- report.member_decisions << TreeHaver::ZipMemberDecision.new(normalized_path: path, operation: operation, disposition: disposition, reason: reason)
323
+ report.member_decisions << TreeHaver::ZipMemberDecision.new(normalized_path: path, operation: operation,
324
+ disposition: disposition, reason: reason)
261
325
  report.merge_report.rewritten_nodes << schema_path(path)
262
326
  end
263
327
 
264
328
  def normalize_zip_path(path)
265
- path.tr("\\", "/").split("/").each_with_object([]) { |part, stack| part == ".." ? stack.pop : stack << part unless part == "." }.join("/")
329
+ path.tr('\\', '/').split('/').each_with_object([]) do |part, stack|
330
+ unless part == '.'
331
+ part == '..' ? stack.pop : stack << part
332
+ end
333
+ end.join('/')
266
334
  end
267
335
 
268
336
  def compression_name(method)
269
337
  case method
270
- when 0 then "stored"
271
- when 8 then "deflate"
338
+ when 0 then 'stored'
339
+ when 8 then 'deflate'
272
340
  else "method-#{method}"
273
341
  end
274
342
  end
275
343
 
276
344
  def escapes_root?(path)
277
- path.start_with?("/") || path.tr("\\", "/").split("/").then { |parts| depth = 0; parts.any? { |part| part == ".." ? (depth -= 1) : (depth += 1 unless part == "."); depth.negative? } }
345
+ path.start_with?('/') || path.tr('\\', '/').split('/').then do |parts|
346
+ depth = 0
347
+ parts.any? do |part|
348
+ part == '..' ? (depth -= 1) : (depth += 1 unless part == '.')
349
+ depth.negative?
350
+ end
351
+ end
278
352
  end
279
353
 
280
354
  def signing_sensitive?(path)
281
- path.upcase.start_with?("META-INF/") && [".RSA", ".DSA", ".EC", ".SF"].any? { |suffix| path.upcase.end_with?(suffix) }
355
+ path.upcase.start_with?('META-INF/') && ['.RSA', '.DSA', '.EC', '.SF'].any? do |suffix|
356
+ path.upcase.end_with?(suffix)
357
+ end
282
358
  end
283
359
 
284
360
  def same_entry?(left, right)
@@ -290,10 +366,11 @@ module Zip
290
366
  end
291
367
 
292
368
  def nested_family(path)
293
- return "markdown" if path.match?(/\.m(?:d|arkdown)\z/i)
294
- return "json" if path.end_with?(".json")
295
- return "yaml" if path.match?(/\.ya?ml\z/i)
296
- return "xml" if path.end_with?(".xml")
369
+ return 'markdown' if path.match?(/\.m(?:d|arkdown)\z/i)
370
+ return 'json' if path.end_with?('.json')
371
+ return 'yaml' if path.match?(/\.ya?ml\z/i)
372
+
373
+ 'xml' if path.end_with?('.xml')
297
374
  end
298
375
 
299
376
  def schema_path(path)
@@ -301,7 +378,7 @@ module Zip
301
378
  end
302
379
 
303
380
  def diagnostic(category, schema_path, message)
304
- TreeHaver::BinaryDiagnostic.new(severity: "error", category: category, message: message, schema_path: schema_path)
381
+ Binary::Merge.unsafe_diagnostic(schema_path: schema_path, byte_range: nil, message: message, category: category)
305
382
  end
306
383
 
307
384
  def render_error(category, schema_path, message)
@@ -309,3 +386,10 @@ module Zip
309
386
  end
310
387
  end
311
388
  end
389
+
390
+ require 'binary/merge'
391
+ require_relative 'merge/backend'
392
+ require_relative 'merge/provider'
393
+
394
+ Zip::Merge.register_backend!
395
+ Zip::Merge.register_provider!
data/lib/zip-merge.rb CHANGED
@@ -1,3 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "zip/merge"
3
+ require_relative 'zip/merge'
4
+ require 'version_gem'
5
+ require_relative 'zip/merge/version'
6
+
7
+ Zip::Merge::Version.class_eval do
8
+ extend VersionGem::Basic
9
+ end
data/sig/zip/merge.rbs ADDED
@@ -0,0 +1,21 @@
1
+ module Zip
2
+ module Merge
3
+ class Provider < Binary::Merge::Provider
4
+ def provider_id: () -> String
5
+ def family: () -> String
6
+ def capabilities: () -> Hash[Symbol, untyped]
7
+ def analyze: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
8
+ def diff2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
9
+ def merge2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
10
+ def merge3: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
11
+ end
12
+
13
+ def self.merge_provider: () -> Provider
14
+ def self.register_provider!: () -> Hash[Symbol, untyped]
15
+
16
+ module Version
17
+ VERSION: String
18
+ end
19
+ VERSION: String
20
+ end
21
+ end
data.tar.gz.sig CHANGED
Binary file