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 +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/omnizip/algorithms/deflate.rb +8 -2
- data/lib/omnizip/algorithms/deflate64.rb +9 -4
- data/lib/omnizip/algorithms/zstandard.rb +4 -2
- data/lib/omnizip/backends.rb +13 -2
- data/lib/omnizip/formats/gzip.rb +27 -18
- data/lib/omnizip/formats/rar/parity_handler.rb +19 -12
- data/lib/omnizip/formats/xz.rb +9 -3
- data/lib/omnizip/implementations/rust/library.rb +11 -2
- data/lib/omnizip/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34bab2534b75e9ca958c00130c6b5962dbd5422bd6e0f4683c8e756415d9961b
|
|
4
|
+
data.tar.gz: 9089d88fbe8ce8bda17fecd2a21454db2daa23c86e930b69cf8bb86a495b1ce0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
102
|
-
decompressed =
|
|
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
|
|
data/lib/omnizip/backends.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/omnizip/formats/gzip.rb
CHANGED
|
@@ -91,18 +91,15 @@ module Omnizip
|
|
|
91
91
|
# Write GZIP header
|
|
92
92
|
write_header(output_io, mtime, original_name)
|
|
93
93
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
|
|
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(
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
data/lib/omnizip/formats/xz.rb
CHANGED
|
@@ -89,11 +89,17 @@ module Omnizip
|
|
|
89
89
|
|
|
90
90
|
case format
|
|
91
91
|
when :lz
|
|
92
|
-
|
|
92
|
+
Backends.decompress("lzip", data, Backends::UNKNOWN_LENGTH) do
|
|
93
|
+
decode_lzip(data)
|
|
94
|
+
end
|
|
93
95
|
when :lzma_alone
|
|
94
|
-
|
|
96
|
+
Backends.decompress("lzma-alone", data, Backends::UNKNOWN_LENGTH) do
|
|
97
|
+
decode_lzma_alone(data)
|
|
98
|
+
end
|
|
95
99
|
when :xz
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/omnizip/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-09-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|