omnizip 0.4.2 → 0.4.4
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 +7 -0
- data/lib/omnizip/algorithms/deflate.rb +15 -4
- data/lib/omnizip/algorithms/deflate64.rb +22 -16
- data/lib/omnizip/algorithms/zstandard.rb +11 -4
- data/lib/omnizip/backends.rb +36 -12
- data/lib/omnizip/formats/gzip.rb +27 -18
- data/lib/omnizip/formats/xz.rb +9 -3
- data/lib/omnizip/implementations/rust/library.rb +11 -2
- data/lib/omnizip/implementations/rust.rb +5 -5
- data/lib/omnizip/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8cf4c62c7304ef792b48b96486685707c68010f0cc6529b6a7504454b94ea025
|
|
4
|
+
data.tar.gz: 7c0d5e62fc21a99ccc85520ee0cd3ea32afaae39e76f10826dd56697ffa8376b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 539124e9189208423daacdac7cad6735f4a1069358a6ee7e39bb09155524589a2b1a01a6b1c41af6b650648dee327b2c4d36e71ccbbba5ff5e4453168f6ec9ef
|
|
7
|
+
data.tar.gz: 02ae0fdb9844f006e5289d8bd351b4ac023e8fafd704bd6fc96b6a0489bc27c96d3e7d3e32ef06011a8123b37573d877191e93cdbaa7c0f6e63e139e8485c09e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ 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
|
+
|
|
10
17
|
### Fixed
|
|
11
18
|
- RAR recovery-record repair fails loudly instead of pretending:
|
|
12
19
|
`recover_with_reed_solomon` returned nil and `recover_with_xor`
|
|
@@ -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
|
|
@@ -66,8 +68,13 @@ module Omnizip
|
|
|
66
68
|
# @return [void]
|
|
67
69
|
def compress(input_stream, output_stream, options = nil)
|
|
68
70
|
input_data = input_stream.read
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
level = build_encoder_options(options)[:level] || 6
|
|
72
|
+
output_stream.write(
|
|
73
|
+
Backends.compress("zlib", input_data, level) do
|
|
74
|
+
encoder = Encoder.new(output_stream, build_encoder_options(options))
|
|
75
|
+
encoder.encode_stream(input_data)
|
|
76
|
+
end,
|
|
77
|
+
)
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
# Decompress Deflate-compressed data
|
|
@@ -78,8 +85,12 @@ module Omnizip
|
|
|
78
85
|
# @return [void]
|
|
79
86
|
def decompress(input_stream, output_stream, _options = nil)
|
|
80
87
|
output_stream.set_encoding(Encoding::BINARY)
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
compressed = input_stream.read
|
|
89
|
+
# The encoder's streams carry the zlib wrapper (RFC 1950) —
|
|
90
|
+
# the Rust "zlib" name speaks exactly that framing.
|
|
91
|
+
decompressed = Backends.decompress("zlib", compressed, Backends::UNKNOWN_LENGTH) do
|
|
92
|
+
Decoder.new(StringIO.new(compressed)).decode_stream
|
|
93
|
+
end
|
|
83
94
|
output_stream.write(decompressed)
|
|
84
95
|
end
|
|
85
96
|
|
|
@@ -42,21 +42,22 @@ module Omnizip
|
|
|
42
42
|
# @param options [Hash] Compression options
|
|
43
43
|
# @option options [Integer] :level Compression level (1-9)
|
|
44
44
|
def compress(input, output, options = {})
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
raw_level = options[:level] || Zlib::DEFAULT_COMPRESSION
|
|
47
46
|
data = input.read
|
|
48
47
|
return if data.nil? || data.empty?
|
|
49
48
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Zlib::
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
# Zlib::DEFAULT_COMPRESSION (-1) maps to the FFI's 6.
|
|
50
|
+
level = raw_level.negative? ? 6 : raw_level.clamp(0, 9)
|
|
51
|
+
compressed = Backends.compress("zlib", data, level) do
|
|
52
|
+
deflater = Zlib::Deflate.new(
|
|
53
|
+
raw_level,
|
|
54
|
+
Zlib::MAX_WBITS,
|
|
55
|
+
Zlib::MAX_MEM_LEVEL,
|
|
56
|
+
)
|
|
57
|
+
result = deflater.deflate(data, Zlib::FINISH)
|
|
58
|
+
deflater.close
|
|
59
|
+
result
|
|
60
|
+
end
|
|
60
61
|
output.write(compressed)
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -73,10 +74,15 @@ module Omnizip
|
|
|
73
74
|
output.set_encoding(Encoding::BINARY)
|
|
74
75
|
output.binmode
|
|
75
76
|
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
# The streams this algorithm produces/consumes are zlib
|
|
78
|
+
# containers (RFC 1950); the Rust "zlib" name speaks exactly
|
|
79
|
+
# that, so the tier swap is byte-transparent.
|
|
80
|
+
decompressed = Backends.decompress("zlib", compressed, Backends::UNKNOWN_LENGTH) do
|
|
81
|
+
inflater = Zlib::Inflate.new(Zlib::MAX_WBITS)
|
|
82
|
+
result = inflater.inflate(compressed)
|
|
83
|
+
inflater.close
|
|
84
|
+
result
|
|
85
|
+
end
|
|
80
86
|
|
|
81
87
|
# Force binary encoding to match original data
|
|
82
88
|
decompressed.force_encoding(Encoding::BINARY)
|
|
@@ -86,8 +86,13 @@ module Omnizip
|
|
|
86
86
|
# @return [void]
|
|
87
87
|
def compress(input_stream, output_stream, options = nil)
|
|
88
88
|
input_data = input_stream.read
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
level = build_encoder_options(options)[:level] || 3
|
|
90
|
+
output_stream.write(
|
|
91
|
+
Backends.compress("zstd", input_data, level) do
|
|
92
|
+
encoder = Encoder.new(output_stream, build_encoder_options(options))
|
|
93
|
+
encoder.encode_stream(input_data)
|
|
94
|
+
end,
|
|
95
|
+
)
|
|
91
96
|
end
|
|
92
97
|
|
|
93
98
|
# Decompress Zstandard-compressed data
|
|
@@ -98,8 +103,10 @@ module Omnizip
|
|
|
98
103
|
# @return [void]
|
|
99
104
|
def decompress(input_stream, output_stream, _options = nil)
|
|
100
105
|
output_stream.set_encoding(Encoding::BINARY)
|
|
101
|
-
|
|
102
|
-
decompressed =
|
|
106
|
+
compressed = input_stream.read
|
|
107
|
+
decompressed = Backends.decompress("zstd", compressed, Backends::UNKNOWN_LENGTH) do
|
|
108
|
+
Decoder.new(StringIO.new(compressed)).decode_stream
|
|
109
|
+
end
|
|
103
110
|
output_stream.write(decompressed)
|
|
104
111
|
end
|
|
105
112
|
|
data/lib/omnizip/backends.rb
CHANGED
|
@@ -27,19 +27,23 @@ module Omnizip
|
|
|
27
27
|
# over frame stability.
|
|
28
28
|
module Backends
|
|
29
29
|
# Codecs whose Rust encoder is BYTE-IDENTICAL to the pure-Ruby
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
30
|
+
# 2026-09-20 policy flip (owner directive): RUST IS THE AUTHORITY
|
|
31
|
+
# — its encoders/decoders are the ports validated against the
|
|
32
|
+
# reference C/C++ implementations (bzip2, xz/liblzma, zstd,
|
|
33
|
+
# libdeflate), and they are determinism-contracted (same input +
|
|
34
|
+
# level => same bytes on every machine). :auto therefore routes
|
|
35
|
+
# BOTH directions through Rust whenever the cdylib loads; encode
|
|
36
|
+
# falls back to the Ruby core on any Rust error, so :auto is
|
|
37
|
+
# never worse than pure Ruby. The pure-Ruby cores remain the
|
|
38
|
+
# portable fallback and the OMNIZIP_BACKEND=ruby escape hatch.
|
|
39
|
+
RUST_AUTHORITATIVE = true
|
|
36
40
|
|
|
37
41
|
# usize::MAX — the FFI's "caller does not know the plaintext
|
|
38
42
|
# size" sentinel (streaming decode contract).
|
|
39
43
|
UNKNOWN_LENGTH = 0xFFFF_FFFF_FFFF_FFFF
|
|
40
44
|
|
|
41
45
|
class << self
|
|
42
|
-
def for(codec,
|
|
46
|
+
def for(codec, _direction)
|
|
43
47
|
mode = ENV.fetch("OMNIZIP_BACKEND", "auto")
|
|
44
48
|
|
|
45
49
|
return backend_rust if mode == "rust"
|
|
@@ -48,26 +52,46 @@ module Omnizip
|
|
|
48
52
|
return backend_ruby unless Implementations::Rust::Library.available?
|
|
49
53
|
return backend_ruby unless Implementations::Rust::Library::CODECS.include?(codec)
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
backend_rust
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
# Compress through the tier-selected backend. The block is
|
|
55
59
|
# the pure-Ruby core — evaluated ONLY on the Ruby path, so
|
|
56
|
-
# the accelerated path never pays for it.
|
|
60
|
+
# the accelerated path never pays for it. A Rust encode error
|
|
61
|
+
# falls back to the Ruby core (auto is never worse than pure
|
|
62
|
+
# Ruby); only the forced `rust` mode propagates.
|
|
57
63
|
def compress(codec, data, level, &ruby_core)
|
|
58
64
|
backend = Backends.for(codec, :encode)
|
|
59
65
|
return yield if backend == RubyBackend
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
begin
|
|
68
|
+
backend.compress(codec, data, level)
|
|
69
|
+
rescue Implementations::Rust::Error => e
|
|
70
|
+
raise if ENV.fetch("OMNIZIP_BACKEND", "auto") == "rust"
|
|
71
|
+
|
|
72
|
+
warn "omnizip: rust #{codec} encode failed (#{e.message}); using ruby" if ENV["OMNIZIP_BACKEND_DEBUG"]
|
|
73
|
+
yield
|
|
74
|
+
end
|
|
62
75
|
end
|
|
63
76
|
|
|
64
77
|
# Decompress through the tier-selected backend (output-
|
|
65
|
-
# invariant: :auto prefers Rust whenever loadable).
|
|
78
|
+
# invariant: :auto prefers Rust whenever loadable). A Rust
|
|
79
|
+
# failure falls back to the Ruby core — the Ruby readers are
|
|
80
|
+
# the tolerant superset (legacy container variants, malformed
|
|
81
|
+
# inputs they historically accept), so :auto must never be
|
|
82
|
+
# WORSE than pure Ruby. Only the forced `rust` mode propagates.
|
|
66
83
|
def decompress(codec, data, expected_len, &ruby_core)
|
|
67
84
|
backend = Backends.for(codec, :decode)
|
|
68
85
|
return yield if backend == RubyBackend
|
|
69
86
|
|
|
70
|
-
|
|
87
|
+
begin
|
|
88
|
+
backend.decompress(codec, data, expected_len)
|
|
89
|
+
rescue Implementations::Rust::Error => e
|
|
90
|
+
raise if ENV.fetch("OMNIZIP_BACKEND", "auto") == "rust"
|
|
91
|
+
|
|
92
|
+
warn "omnizip: rust #{codec} decode failed (#{e.message}); using ruby" if ENV["OMNIZIP_BACKEND_DEBUG"]
|
|
93
|
+
yield
|
|
94
|
+
end
|
|
71
95
|
end
|
|
72
96
|
|
|
73
97
|
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
|
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
|
|
20
|
+
# surface (both directions by default — Rust is the
|
|
21
|
+
# authority; Ruby is the fallback).
|
|
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
|
|
|
@@ -13,11 +13,11 @@ module Omnizip
|
|
|
13
13
|
# 1. ENV['OMNIZIP_FFI_DYLIB'] (explicit path)
|
|
14
14
|
# 2. ../target/release/ inside a sibling omnizip-rs checkout
|
|
15
15
|
#
|
|
16
|
-
# Tier policy (see Omnizip::Backends):
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
16
|
+
# Tier policy (see Omnizip::Backends): Rust is the authority —
|
|
17
|
+
# validated against the reference C/C++ implementations and
|
|
18
|
+
# determinism-contracted — so `auto` routes BOTH directions
|
|
19
|
+
# through Rust whenever the library loads, with Ruby-core
|
|
20
|
+
# fallback on any Rust error.
|
|
21
21
|
module Rust
|
|
22
22
|
autoload :Library, "omnizip/implementations/rust/library"
|
|
23
23
|
end
|
data/lib/omnizip/version.rb
CHANGED