omnizip 0.3.37 → 0.3.38

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.
@@ -60,7 +60,9 @@ module Omnizip
60
60
  entry.source_path = nil
61
61
  entry.size = data_str.bytesize
62
62
  entry.mtime = Time.now
63
- entry.has_stream = true
63
+ # Zero-length data has no stream: readers identify it as an
64
+ # empty file through kEmptyStream + kEmptyFile.
65
+ entry.has_stream = data_str.bytesize.positive?
64
66
  entry.inline_data = data_str
65
67
  entry.compression_options = options
66
68
  @entries << entry
@@ -280,7 +282,7 @@ module Omnizip
280
282
  end
281
283
 
282
284
  def build_solid_streams_info(metadata, unpack_size, compressed_size,
283
- num_files)
285
+ _num_files)
284
286
  # kPackInfo property (0x06)
285
287
  metadata << [PropertyId::PACK_INFO].pack("C")
286
288
  metadata << write_number(0) # Pack position
@@ -313,37 +315,44 @@ num_files)
313
315
  # kEnd for UnpackInfo
314
316
  metadata << [PropertyId::K_END].pack("C")
315
317
 
316
- # kSubStreamsInfo - for solid archives with multiple files
318
+ # kSubStreamsInfo describes exactly the entries WITH a
319
+ # stream: directory entries (empty-stream) are excluded,
320
+ # or readers reject the header.
317
321
  metadata << [PropertyId::SUBSTREAMS_INFO].pack("C")
318
- if num_files > 1
319
-
320
- # NUM_UNPACK_STREAM - number of files in this folder
322
+ stream_files = @entries.select(&:has_stream?)
323
+ # Write the explicit form whenever any entry lacks a
324
+ # stream (directory entries): without kNumUnpackStreams,
325
+ # readers assume one substream per file and reject the
326
+ # digest count.
327
+ if stream_files.size > 1 || stream_files.size != @entries.size
328
+
329
+ # NUM_UNPACK_STREAM - number of substreams in this folder
321
330
  metadata << [PropertyId::NUM_UNPACK_STREAM].pack("C")
322
- metadata << write_number(num_files)
331
+ metadata << write_number(stream_files.size)
323
332
 
324
- # SIZE - size of each file's data (except the last one)
325
- # Per 7-Zip spec: only write numSubstreams-1 sizes, last is calculated
326
- # from folder's unpack size minus sum of written sizes
333
+ # SIZE - size of each substream except the last one
334
+ # Per 7-Zip spec: only write numSubstreams-1 sizes, last
335
+ # is calculated from folder's unpack size minus the sum
327
336
  metadata << [PropertyId::SIZE].pack("C")
328
- @entries[0..-2].each do |entry| # All except last
337
+ stream_files[0..-2].each do |entry|
329
338
  metadata << write_number(entry.size)
330
339
  end
331
340
 
332
341
  # CRCs
333
342
  metadata << [PropertyId::CRC].pack("C")
334
343
  metadata << [1].pack("C") # All defined
335
- @entries.each do |entry|
344
+ stream_files.each do |entry|
336
345
  # Use 0 for entries without CRC (empty files)
337
346
  crc = entry.crc || 0
338
347
  metadata << [crc].pack("V")
339
348
  end
340
349
 
341
350
  else
342
- # Single file: CRC goes in SubStreamsInfo
351
+ # Single substream: CRC goes in SubStreamsInfo
343
352
  metadata << [PropertyId::CRC].pack("C")
344
353
  metadata << [1].pack("C") # All defined
345
354
  # Use 0 for entries without CRC (empty files)
346
- crc = @entries.first&.crc || 0
355
+ crc = stream_files.first&.crc || 0
347
356
  metadata << [crc].pack("V")
348
357
  end
349
358
  metadata << [PropertyId::K_END].pack("C")
@@ -396,12 +405,13 @@ num_files)
396
405
  # kEnd for UnpackInfo
397
406
  metadata << [PropertyId::K_END].pack("C")
398
407
 
399
- # kSubStreamsInfo - CRCs for each file
408
+ # kSubStreamsInfo - CRC for each substream. Each folder
409
+ # holds exactly one substream here, so digests cover only
410
+ # the entries WITH a stream (directory entries have none).
400
411
  metadata << [PropertyId::SUBSTREAMS_INFO].pack("C")
401
412
  metadata << [PropertyId::CRC].pack("C")
402
413
  metadata << [1].pack("C") # All defined
403
- @entries.each do |entry|
404
- # Use 0 for entries without CRC (empty files)
414
+ @entries.select(&:has_stream?).each do |entry|
405
415
  crc = entry.crc || 0
406
416
  metadata << [crc].pack("V")
407
417
  end
@@ -438,6 +448,34 @@ num_files)
438
448
  # Number of files
439
449
  metadata << write_number(@entries.size)
440
450
 
451
+ # Build EMPTY_STREAM property (0x0E) — written first, like
452
+ # 7-Zip itself: readers derive the directory flag from it;
453
+ # without it, directory entries are extracted as (invalid)
454
+ # files. Format: [EMPTY_STREAM] [size] [bit vector]
455
+ empty_bits = @entries.map { |e| !e.has_stream? }
456
+ if empty_bits.any?
457
+ # kEmptyStream carries the raw bit bytes only — NO
458
+ # all-defined marker byte (verified against 7-Zip's own
459
+ # -mhc=off output; a marker byte here shifts the whole
460
+ # header for strict readers).
461
+ empty_data = write_bit_bytes(empty_bits)
462
+ metadata << [PropertyId::EMPTY_STREAM].pack("C")
463
+ metadata << write_number(empty_data.bytesize)
464
+ metadata << empty_data
465
+
466
+ # kEmptyFile distinguishes zero-byte files from
467
+ # directories among the empty-stream entries.
468
+ empty_file_bits = @entries.map do |e|
469
+ !e.has_stream? && !e.is_dir
470
+ end
471
+ if empty_file_bits.any?
472
+ ef_data = write_bit_bytes(empty_file_bits)
473
+ metadata << [PropertyId::EMPTY_FILE].pack("C")
474
+ metadata << write_number(ef_data.bytesize)
475
+ metadata << ef_data
476
+ end
477
+ end
478
+
441
479
  # Build NAME property (0x11)
442
480
  # Format: [NAME] [size] [external] [UTF-16LE names with null terminators]
443
481
  name_data = String.new(encoding: "BINARY")
@@ -474,8 +512,8 @@ num_files)
474
512
  # Build WIN_ATTRIB property (0x15) - comes after MTIME per 7-Zip spec
475
513
  # Format: [WIN_ATTRIB] [size] [defined bits] [external] [attributes]
476
514
  attr_data = String.new(encoding: "BINARY")
477
- @entries.each do |_entry|
478
- attr_data << FILE_ATTRIBUTE_ARCHIVE
515
+ @entries.each do |entry|
516
+ attr_data << [entry.attributes || FILE_ATTRIBUTE_ARCHIVE.unpack1("V")].pack("V")
479
517
  end
480
518
 
481
519
  metadata << [PropertyId::WIN_ATTRIB].pack("C")
@@ -671,6 +709,27 @@ next_header_data)
671
709
  # 110xxxxx BYTE y[2] : value = (xxxxx << 16) + y
672
710
  # 1110xxxx BYTE y[3] : value = (xxxx << 24) + y
673
711
  # ...up to 8 bytes total
712
+ # Raw MSB-first packed bit bytes, no marker
713
+ def write_bit_bytes(bits)
714
+ bytes = Array.new((bits.size + 7) / 8, 0)
715
+ bits.each_with_index do |bit, i|
716
+ bytes[i / 8] |= (1 << (7 - (i % 8))) if bit
717
+ end
718
+ bytes.pack("C*")
719
+ end
720
+
721
+ # Bit vector with an all-defined marker byte, MSB-first
722
+ # packing (same layout as HeaderWriter's)
723
+ def write_bit_vector(bits)
724
+ return [1].pack("C") if bits.all?
725
+
726
+ bytes = Array.new((bits.size + 7) / 8, 0)
727
+ bits.each_with_index do |bit, i|
728
+ bytes[i / 8] |= (1 << (7 - (i % 8))) if bit
729
+ end
730
+ [0].pack("C") + bytes.pack("C*")
731
+ end
732
+
674
733
  def write_number(value)
675
734
  # Single byte encoding (0-127)
676
735
  return [value].pack("C") if value < 0x80
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.37"
4
+ VERSION = "0.3.38"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnizip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.37
4
+ version: 0.3.38
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -324,6 +324,8 @@ files:
324
324
  - lib/omnizip/archive/reader_session.rb
325
325
  - lib/omnizip/archive_handler.rb
326
326
  - lib/omnizip/archive_handlers.rb
327
+ - lib/omnizip/archive_handlers/cpio_handler.rb
328
+ - lib/omnizip/archive_handlers/iso_handler.rb
327
329
  - lib/omnizip/archive_handlers/rar_handler.rb
328
330
  - lib/omnizip/archive_handlers/seven_zip_handler.rb
329
331
  - lib/omnizip/archive_handlers/tar_handler.rb