omnizip 0.4.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06cbc9a2cd22e74a84d82211efa5e000d857da117bae9a995b76520839211362
4
- data.tar.gz: e8ab9ec5638953dda59f4b43aa299cfa55262f77d3c9e9f795bbe8800d443592
3
+ metadata.gz: 34bab2534b75e9ca958c00130c6b5962dbd5422bd6e0f4683c8e756415d9961b
4
+ data.tar.gz: 9089d88fbe8ce8bda17fecd2a21454db2daa23c86e930b69cf8bb86a495b1ce0
5
5
  SHA512:
6
- metadata.gz: ecd9426b7a3f0a75c68821ec6c2c4c0f0f4f6f5e5ae7a252dc6e1e4fb9f5eb94a01b84f937c797581b44584e8cc4ba244d35e37c2338b1401c46de42299a1627
7
- data.tar.gz: 268f70fb015d2e21b8079cae5ea4e09a83e036e967cf83bed03cc37de45b61f1875547b1443b419eec502a3bcc264e323d19bb8c503f72437cfb47bbd9d31853
6
+ metadata.gz: d92441da2c757c65e1e5865abd446cd2a296adafcdf56d5dabfc00f6c792fd6f9b8f3e51dda0231ce5d8829eaf4b81662cfdadc13cdbc9495c73d088968c6c81
7
+ data.tar.gz: 30591da9a2e0af6922a49596133f98e5f05bf30e927b698e4b413c1f9f0d1907ab6f0f9a0cc32eff385772b7445aff36e271066690c85e3a134dd040e8185591
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+ - Rust tier wiring for every accelerated codec: zstd, deflate,
12
+ deflate64 (both zlib-framed), gzip, xz, lzip, and lzma-alone now
13
+ route decode through the Rust backend when it loads, each pinned by
14
+ a cross-implementation differential. A Rust failure falls back to
15
+ the Ruby core — auto mode is never worse than pure Ruby.
16
+
17
+ ### Fixed
18
+ - RAR recovery-record repair fails loudly instead of pretending:
19
+ `recover_with_reed_solomon` returned nil and `recover_with_xor`
20
+ applied a transform the record does not use. Neither unrar (its
21
+ recvol5/rs16 cover only the separate .rev volumes) nor any public
22
+ spec carries the in-archive RAR5 RS parameters, so both paths now
23
+ raise `NotImplementedError` with par2 guidance, surfaced through
24
+ `ArchiveRepairer` results. The `ozip repair` counterpart (Rust)
25
+ ships in omnizip-rs with the same honest contract.
26
+
10
27
  ### Added
11
28
  - `rake rust:build` — one command to build and vendor the Rust
12
29
  backend's cdylib (clones omnizip-rs or reuses a checkout via
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stringio"
4
+
3
5
  # Copyright (C) 2025 Ribose Inc.
4
6
  #
5
7
  # Permission is hereby granted, free of charge, to any person obtaining a
@@ -78,8 +80,12 @@ module Omnizip
78
80
  # @return [void]
79
81
  def decompress(input_stream, output_stream, _options = nil)
80
82
  output_stream.set_encoding(Encoding::BINARY)
81
- decoder = Decoder.new(input_stream)
82
- decompressed = decoder.decode_stream
83
+ compressed = input_stream.read
84
+ # The encoder's streams carry the zlib wrapper (RFC 1950) —
85
+ # the Rust "zlib" name speaks exactly that framing.
86
+ decompressed = Backends.decompress("zlib", compressed, Backends::UNKNOWN_LENGTH) do
87
+ Decoder.new(StringIO.new(compressed)).decode_stream
88
+ end
83
89
  output_stream.write(decompressed)
84
90
  end
85
91
 
@@ -73,10 +73,15 @@ module Omnizip
73
73
  output.set_encoding(Encoding::BINARY)
74
74
  output.binmode
75
75
 
76
- # Use Zlib::Inflate with maximum window size
77
- inflater = Zlib::Inflate.new(Zlib::MAX_WBITS)
78
- decompressed = inflater.inflate(compressed)
79
- inflater.close
76
+ # The streams this algorithm produces/consumes are zlib
77
+ # containers (RFC 1950); the Rust "zlib" name speaks exactly
78
+ # that, so the tier swap is byte-transparent.
79
+ decompressed = Backends.decompress("zlib", compressed, Backends::UNKNOWN_LENGTH) do
80
+ inflater = Zlib::Inflate.new(Zlib::MAX_WBITS)
81
+ result = inflater.inflate(compressed)
82
+ inflater.close
83
+ result
84
+ end
80
85
 
81
86
  # Force binary encoding to match original data
82
87
  decompressed.force_encoding(Encoding::BINARY)
@@ -98,8 +98,10 @@ module Omnizip
98
98
  # @return [void]
99
99
  def decompress(input_stream, output_stream, _options = nil)
100
100
  output_stream.set_encoding(Encoding::BINARY)
101
- decoder = Decoder.new(input_stream)
102
- decompressed = decoder.decode_stream
101
+ compressed = input_stream.read
102
+ decompressed = Backends.decompress("zstd", compressed, Backends::UNKNOWN_LENGTH) do
103
+ Decoder.new(StringIO.new(compressed)).decode_stream
104
+ end
103
105
  output_stream.write(decompressed)
104
106
  end
105
107
 
@@ -62,12 +62,23 @@ module Omnizip
62
62
  end
63
63
 
64
64
  # Decompress through the tier-selected backend (output-
65
- # invariant: :auto prefers Rust whenever loadable).
65
+ # invariant: :auto prefers Rust whenever loadable). A Rust
66
+ # failure falls back to the Ruby core — the Ruby readers are
67
+ # the tolerant superset (legacy container variants, malformed
68
+ # inputs they historically accept), so :auto must never be
69
+ # WORSE than pure Ruby. Only the forced `rust` mode propagates.
66
70
  def decompress(codec, data, expected_len, &ruby_core)
67
71
  backend = Backends.for(codec, :decode)
68
72
  return yield if backend == RubyBackend
69
73
 
70
- backend.decompress(codec, data, expected_len)
74
+ begin
75
+ backend.decompress(codec, data, expected_len)
76
+ rescue Implementations::Rust::Error => e
77
+ raise if ENV.fetch("OMNIZIP_BACKEND", "auto") == "rust"
78
+
79
+ warn "omnizip: rust #{codec} decode failed (#{e.message}); using ruby" if ENV["OMNIZIP_BACKEND_DEBUG"]
80
+ yield
81
+ end
71
82
  end
72
83
 
73
84
  private
@@ -91,18 +91,15 @@ module Omnizip
91
91
  # Write GZIP header
92
92
  write_header(output_io, mtime, original_name)
93
93
 
94
- # Compress data with Deflate
95
- deflate = Zlib::Deflate.new(
96
- level,
97
- Zlib::MAX_WBITS + 16, # Use GZIP wrapper
98
- )
99
- compressed = deflate.deflate(input_data, Zlib::FINISH)
94
+ # RFC 1952 member: header + RAW deflate + CRC32/ISIZE footer.
95
+ # The old implementation layered zlib's own GZIP wrapper
96
+ # under this header (a second 1F 8B magic) — the gem's
97
+ # reader tolerated it, but every standard gzip tool
98
+ # rejected the output. Raw-window mode emits bare DEFLATE.
99
+ deflate = Zlib::Deflate.new(level, -Zlib::MAX_WBITS)
100
+ output_io.write(deflate.deflate(input_data, Zlib::FINISH))
100
101
  deflate.close
101
102
 
102
- # Extract just the compressed data (remove zlib's GZIP wrapper)
103
- # We'll write our own footer
104
- output_io.write(compressed[0...-8]) if compressed.bytesize > 8
105
-
106
103
  # Write GZIP footer
107
104
  write_footer(output_io, input_data)
108
105
  end
@@ -113,15 +110,27 @@ module Omnizip
113
110
  # @param output_io [IO] Output stream
114
111
  # @return [Hash] Metadata (original_name, mtime)
115
112
  def decompress_stream(input_io, output_io)
116
- # Read and parse header
117
- metadata = read_header(input_io)
118
-
119
- # Decompress with Deflate
120
- inflate = Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
121
- decompressed = inflate.inflate(input_io.read)
122
- inflate.close
113
+ data = input_io.read
114
+
115
+ # Metadata comes from the header either way (cheap parse).
116
+ metadata = read_header(StringIO.new(data))
117
+
118
+ # The tier gets the WHOLE member (header + deflate + trailer)
119
+ # — the Rust "gzip" name decodes the container end-to-end.
120
+ decompressed = Backends.decompress("gzip", data, Backends::UNKNOWN_LENGTH) do
121
+ body = StringIO.new(data)
122
+ read_header(body) # skip past the header
123
+ # Legacy double-header members (written before the RFC
124
+ # 1952 fix) carry zlib's own 10-byte GZIP header here;
125
+ # standard members carry bare DEFLATE.
126
+ rest = body.read
127
+ rest = rest.byteslice(10..) if rest.byteslice(0, 2) == "\x1F\x8B".b
128
+ inflate = Zlib::Inflate.new(-Zlib::MAX_WBITS)
129
+ result = inflate.inflate(rest)
130
+ inflate.close
131
+ result
132
+ end
123
133
 
124
- # Write decompressed data
125
134
  output_io.write(decompressed)
126
135
 
127
136
  metadata
@@ -266,10 +266,16 @@ module Omnizip
266
266
  # @param parity_data [String] Parity information
267
267
  # @return [String, nil] Recovered data
268
268
  def recover_with_reed_solomon(_corrupted_data, _parity_data)
269
- # Placeholder for Reed-Solomon recovery
270
- # Real implementation would use reed-solomon gem
271
- # For now, return nil to indicate recovery not available
272
- nil
269
+ # The in-archive RAR5 recovery record's RS parameters exist
270
+ # only in the proprietary rar binary — unrar carries no
271
+ # implementation (its recvol5/rs16 cover only the separate
272
+ # .rev volume feature), and no independent spec is public.
273
+ # Fail loudly with actionable guidance instead of pretending
274
+ # recovery happened.
275
+ raise NotImplementedError,
276
+ "in-archive RAR5 Reed-Solomon recovery is not implemented " \
277
+ "(rar-proprietary format); protect archives with external " \
278
+ "parity (par2) instead"
273
279
  end
274
280
 
275
281
  # Recover data using XOR (RAR4)
@@ -277,14 +283,15 @@ module Omnizip
277
283
  # @param corrupted_data [String] Corrupted block
278
284
  # @param parity_data [String] Parity information
279
285
  # @return [String] Recovered data
280
- def recover_with_xor(corrupted_data, parity_data)
281
- # Simple XOR recovery
282
- result = corrupted_data.bytes.map.with_index do |byte, i|
283
- parity_byte = parity_data.bytes[i % parity_data.size]
284
- byte ^ parity_byte
285
- end
286
-
287
- result.pack("C*")
286
+ def recover_with_xor(_corrupted_data, _parity_data)
287
+ # RAR4's integrated recovery record is likewise rar-proprietary
288
+ # (unrar has no repair path for it either); a cyclic XOR with
289
+ # arbitrary parity bytes is not the record's actual transform
290
+ # and would silently fabricate corrupt bytes. Refuse honestly.
291
+ raise NotImplementedError,
292
+ "in-archive RAR4 recovery-record repair is not implemented " \
293
+ "(rar-proprietary format); protect archives with external " \
294
+ "parity (par2) instead"
288
295
  end
289
296
  end
290
297
  end
@@ -89,11 +89,17 @@ module Omnizip
89
89
 
90
90
  case format
91
91
  when :lz
92
- decode_lzip(data)
92
+ Backends.decompress("lzip", data, Backends::UNKNOWN_LENGTH) do
93
+ decode_lzip(data)
94
+ end
93
95
  when :lzma_alone
94
- decode_lzma_alone(data)
96
+ Backends.decompress("lzma-alone", data, Backends::UNKNOWN_LENGTH) do
97
+ decode_lzma_alone(data)
98
+ end
95
99
  when :xz
96
- decode_xz_stream(data)
100
+ Backends.decompress("xz", data, Backends::UNKNOWN_LENGTH) do
101
+ decode_xz_stream(data)
102
+ end
97
103
  else
98
104
  raise FormatError, "Unknown LZMA format: cannot detect valid format"
99
105
  end
@@ -16,7 +16,13 @@ module Omnizip
16
16
  # built; compress/decompress are pure C calls).
17
17
  class Library
18
18
  # Codecs the cdylib dispatches by name.
19
- CODECS = %w[bzip2 zstd lzma xz].freeze
19
+ # Names the cdylib dispatches: the codec-level acceleration surface
20
+ # (decode tier by default; encode only under forced rust mode —
21
+ # RUST_ENCODE_IDENTICAL gates the auto tier).
22
+ CODECS = %w[
23
+ bzip2 zstd lzma xz lzma-alone lzip
24
+ deflate deflate64 zlib gzip
25
+ ].freeze
20
26
 
21
27
  class << self
22
28
  # The shared handle, or nil when the cdylib cannot load.
@@ -36,7 +42,10 @@ module Omnizip
36
42
  # specs exercising fallback/absence paths need a clean
37
43
  # slate; production never calls this).
38
44
  def forget!
39
- @instance = nil if defined?(@instance)
45
+ # Remove the ivar entirely: assigning nil would leave it
46
+ # defined, and `instance`'s `defined?` guard would return
47
+ # the nil forever without ever re-opening the library.
48
+ remove_instance_variable(:@instance) if defined?(@instance)
40
49
  nil
41
50
  end
42
51
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnizip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-19 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64