omnizip 0.3.57 → 0.4.0
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 +64 -0
- data/docs/guides/creating-archives.adoc +17 -23
- data/lib/omnizip/algorithms/bzip2.rb +9 -2
- data/lib/omnizip/backends.rb +116 -0
- data/lib/omnizip/commands/decompress_command.rb +4 -1
- data/lib/omnizip/formats/rar/rar5/writer.rb +5 -0
- data/lib/omnizip/formats/zip/writer.rb +40 -0
- data/lib/omnizip/implementations/rust/library.rb +151 -0
- data/lib/omnizip/implementations/rust.rb +25 -0
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip.rb +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a04b0c83ee4e9c6b57823c8db709178ed2b88e073f658fb6573d9f69fa7459f
|
|
4
|
+
data.tar.gz: 1c5d9b7e86c1d1322cfd8a8193fcec2c8b7a4d28a21b6ee4b6fca3a359bc01e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c22355d1136d66e0c6632ce6b4ad57a1b27c8cf826b0f5593318bffd20e85de77ed8072e280a88aa01433ef658fb1adc9139ff1ff8aa6d31735efb6aea1fc9f5
|
|
7
|
+
data.tar.gz: b06259b9ca3ede7ed15bdfa33537a9d9a17099a59ac5b0d5dc95a4f29a46ead222641abf4e8a17f17a283b1272f99d589c3541d799da0f932fc58a1401aa0d2c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,70 @@ 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-accelerated backend tier: `Omnizip::Backends` is the single
|
|
12
|
+
tier switch between the pure-Ruby cores and the `omnizip-ffi`
|
|
13
|
+
cdylib (stdlib Fiddle — no compiled extension). Under the default
|
|
14
|
+
`auto` policy decode prefers the Rust decoders (output-invariant
|
|
15
|
+
and carrying fixes the Ruby cores lack); encode stays pure-Ruby
|
|
16
|
+
so frame bytes never depend on the tier (content addressing
|
|
17
|
+
stability). `OMNIZIP_BACKEND=rust|ruby` forces a tier; load
|
|
18
|
+
failures fall back to pure Ruby silently. A cross-tier
|
|
19
|
+
differential spec pins decode agreement in both encode
|
|
20
|
+
directions.
|
|
21
|
+
- BZip2 routes its byte core through the backend seam (both paths
|
|
22
|
+
share the algorithm class's IO plumbing).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Ruby 4.0 compatibility for the tier loader: fiddle moved out of
|
|
26
|
+
the default gems and is now required lazily with a fallback;
|
|
27
|
+
library resolution no longer references Fiddle constants when
|
|
28
|
+
fiddle is absent and considers `.dll` candidates on Windows.
|
|
29
|
+
|
|
30
|
+
## [0.3.58] - 2026-09-03
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- `solid: true` on the RAR5 writer silently fell back to
|
|
34
|
+
independent STORE entries (the RAR-compatible LZSS encoder is not
|
|
35
|
+
available); the writer now warns about the fallback. The
|
|
36
|
+
creating-archives guide's solid section stops advertising
|
|
37
|
+
compression ratios the option cannot deliver and the stale
|
|
38
|
+
`(v0.5.0)` version-gate labels are gone from shipped features.
|
|
39
|
+
- The ZIP writer never emits ZIP64 structures; past the classic
|
|
40
|
+
limits `Array#pack` failed with a bare `RangeError` that named no
|
|
41
|
+
limit. Both write paths now raise `UnsupportedFormatError` naming
|
|
42
|
+
the 65,535-entry / 4 GiB size / 4 GiB offset classic-ZIP limits
|
|
43
|
+
(ZIP64 writing is not supported).
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- The decompress command's archive-vs-stream routing derives from
|
|
47
|
+
the registry's extension routes instead of a private table, so
|
|
48
|
+
formats added to the registry route correctly instead of silently
|
|
49
|
+
falling into the single-file stream path.
|
|
50
|
+
|
|
51
|
+
## [0.3.57] - 2026-09-02
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- TAR archives no longer corrupt silently on long paths: the writer
|
|
55
|
+
now splits entry names over 99 bytes across the ustar prefix +
|
|
56
|
+
name fields at a "/" boundary, the reader joins them back for
|
|
57
|
+
consumers, and paths no ustar fields can represent raise
|
|
58
|
+
`FormatError` instead of truncating. Verified against system tar.
|
|
59
|
+
- `:auto` profile sampling walked directories in the OS's
|
|
60
|
+
iteration order, resolving different files (and compression
|
|
61
|
+
settings) per platform; the walk is sorted and deterministic now.
|
|
62
|
+
- The getting-started docs' 7z examples used a Reader API that
|
|
63
|
+
never shipped (`each_file`, `close`, `Reader.info`) — every
|
|
64
|
+
snippet raised NoMethodError verbatim. Rewritten against the real
|
|
65
|
+
surface and executed to verify.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
- The last 28 bare `raise "string"` sites (RAR tree, cpio entries,
|
|
69
|
+
parity internals, zip compat streams, two commands) gained their
|
|
70
|
+
matching error classes — `lib/` now contains zero bare raises.
|
|
71
|
+
- The dead `--preserve-ntfs-streams` CLI flag is removed; nothing
|
|
72
|
+
read it and no code path implements NTFS stream preservation.
|
|
73
|
+
|
|
10
74
|
## [0.3.56] - 2026-09-02
|
|
11
75
|
|
|
12
76
|
### Fixed
|
|
@@ -76,7 +76,7 @@ writer.write
|
|
|
76
76
|
|
|
77
77
|
IMPORTANT: RAR5 CRC32 checksums only work with STORE compression. When LZMA is used, CRC32 is automatically disabled. Use BLAKE2sp (always present) for compressed file integrity.
|
|
78
78
|
|
|
79
|
-
=== RAR5 Multi-Volume Archives
|
|
79
|
+
=== RAR5 Multi-Volume Archives
|
|
80
80
|
|
|
81
81
|
Split large archives across multiple volumes:
|
|
82
82
|
|
|
@@ -110,36 +110,30 @@ volumes = writer.write
|
|
|
110
110
|
# Minimum volume size: 64 KB
|
|
111
111
|
----
|
|
112
112
|
|
|
113
|
-
=== RAR5 Solid
|
|
113
|
+
=== RAR5 Solid Archives
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
`solid: true` requests a shared solid block, but the RAR-compatible
|
|
116
|
+
LZSS encoder is not implemented yet — the writer warns and writes
|
|
117
|
+
independent STORE entries instead, so the archive stays extractable
|
|
118
|
+
by official unrar:
|
|
116
119
|
|
|
117
120
|
[source,ruby]
|
|
118
121
|
----
|
|
119
122
|
require 'omnizip/formats/rar/rar5/writer'
|
|
120
123
|
|
|
121
|
-
# Enable solid compression
|
|
122
124
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('project.rar',
|
|
123
|
-
|
|
124
|
-
level: 5,
|
|
125
|
-
solid: true # Shared dictionary across files
|
|
125
|
+
solid: true # warns: falls back to independent STORE entries
|
|
126
126
|
)
|
|
127
127
|
writer.add_directory('source_code/')
|
|
128
128
|
writer.write
|
|
129
|
+
----
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# - Text documents
|
|
134
|
-
# - Similar structured data
|
|
135
|
-
|
|
136
|
-
# Trade-offs:
|
|
137
|
-
# - Cannot extract individual files without decompressing entire block
|
|
138
|
-
# - Corruption may affect multiple files
|
|
139
|
-
# - Slightly longer extraction time for single files
|
|
131
|
+
Solid compression (shared dictionary across files, better ratios
|
|
132
|
+
for similar content) arrives when the RAR-compatible LZSS encoder
|
|
133
|
+
lands.
|
|
140
134
|
----
|
|
141
135
|
|
|
142
|
-
=== RAR5 AES-256 Encryption
|
|
136
|
+
=== RAR5 AES-256 Encryption
|
|
143
137
|
|
|
144
138
|
Password-protect archives with industry-standard encryption:
|
|
145
139
|
|
|
@@ -177,7 +171,7 @@ writer.write
|
|
|
177
171
|
# - Password verification before decryption
|
|
178
172
|
----
|
|
179
173
|
|
|
180
|
-
=== RAR5 PAR2 Recovery Records
|
|
174
|
+
=== RAR5 PAR2 Recovery Records
|
|
181
175
|
|
|
182
176
|
Add error correction for archive recovery:
|
|
183
177
|
|
|
@@ -216,7 +210,7 @@ files = writer.write
|
|
|
216
210
|
# - Works with multi-volume, solid, and encrypted archives
|
|
217
211
|
----
|
|
218
212
|
|
|
219
|
-
=== RAR5 Combined Features
|
|
213
|
+
=== RAR5 Combined Features
|
|
220
214
|
|
|
221
215
|
Use all features together for comprehensive protection:
|
|
222
216
|
|
|
@@ -262,13 +256,13 @@ puts " Total size: #{files.sum { |f| File.size(f) } / 1024 / 1024}MB"
|
|
|
262
256
|
|
|
263
257
|
=== RAR5 Best Practices
|
|
264
258
|
|
|
265
|
-
**For Maximum
|
|
259
|
+
**For Maximum Compatibility:**
|
|
266
260
|
[source,ruby]
|
|
267
261
|
----
|
|
268
262
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
|
|
269
263
|
compression: :lzma,
|
|
270
|
-
level: 5, #
|
|
271
|
-
|
|
264
|
+
level: 5, # Honored by the 7z-style writers; RAR5 entries
|
|
265
|
+
# are STORE until the LZSS encoder lands
|
|
272
266
|
include_mtime: true
|
|
273
267
|
)
|
|
274
268
|
----
|
|
@@ -67,7 +67,10 @@ module Omnizip
|
|
|
67
67
|
# @return [void]
|
|
68
68
|
def compress(input_stream, output_stream, options = nil)
|
|
69
69
|
level = level_from(options)
|
|
70
|
-
|
|
70
|
+
data = input_stream.read
|
|
71
|
+
output_stream.write(
|
|
72
|
+
Backends.compress("bzip2", data, level) { Bz2.compress(data, level) },
|
|
73
|
+
)
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
# Decompress BZip2-compressed data
|
|
@@ -78,7 +81,11 @@ module Omnizip
|
|
|
78
81
|
# @return [void]
|
|
79
82
|
def decompress(input_stream, output_stream, _options = nil)
|
|
80
83
|
output_stream.set_encoding(Encoding::BINARY)
|
|
81
|
-
|
|
84
|
+
compressed = input_stream.read
|
|
85
|
+
plain = Backends.decompress("bzip2", compressed, Backends::UNKNOWN_LENGTH) do
|
|
86
|
+
Bz2.decompress(compressed)
|
|
87
|
+
end
|
|
88
|
+
output_stream.write(plain)
|
|
82
89
|
end
|
|
83
90
|
|
|
84
91
|
private
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
# Byte-level codec backend resolution — THE tier switch
|
|
5
|
+
# (TODO.ref-parity/62). Both paths share the algorithm classes'
|
|
6
|
+
# IO plumbing; only the byte core swaps.
|
|
7
|
+
#
|
|
8
|
+
# Policy:
|
|
9
|
+
# OMNIZIP_BACKEND=ruby force pure Ruby everywhere
|
|
10
|
+
# OMNIZIP_BACKEND=rust require the Rust library (raises when absent)
|
|
11
|
+
# OMNIZIP_BACKEND=auto (default) Rust for directions marked
|
|
12
|
+
# safe, Ruby otherwise, whenever the
|
|
13
|
+
# cdylib is loadable
|
|
14
|
+
#
|
|
15
|
+
# Safety marks (owner guidance 2026-09-19: some Ruby cores are
|
|
16
|
+
# LESS accurate than the Rust ports — be careful which tier is
|
|
17
|
+
# trusted for what):
|
|
18
|
+
# - decompress -> :rust by default. Decode is output-invariant
|
|
19
|
+
# AND the Rust decoders carry fixes the Ruby cores lack;
|
|
20
|
+
# disagreements are treated as Ruby-side accuracy findings
|
|
21
|
+
# (see the cross-tier differential spec), not coin flips.
|
|
22
|
+
# - compress -> :ruby by default. Byte-stability for content
|
|
23
|
+
# addressing (DropId dedup assumes identical frames); the
|
|
24
|
+
# Rust encoders produce DIFFERENT valid streams (probed:
|
|
25
|
+
# bzip2 L6, Ruby 906 B vs Rust 874 B). OMNIZIP_BACKEND=rust
|
|
26
|
+
# opts into Rust encode where correctness-of-ratio is wanted
|
|
27
|
+
# over frame stability.
|
|
28
|
+
module Backends
|
|
29
|
+
# Codecs whose Rust encoder is BYTE-IDENTICAL to the pure-Ruby
|
|
30
|
+
# encoder (empirical, differential-corpus-proven). 2026-09-19
|
|
31
|
+
# probe: bzip2 L6 on a 50 KB corpus => Ruby 906 B vs Rust 874 B —
|
|
32
|
+
# DIFFERENT valid streams. No codec qualifies; encode stays
|
|
33
|
+
# pure-Ruby under :auto so downstream content addressing never
|
|
34
|
+
# sees tier-dependent bytes. Force with OMNIZIP_BACKEND=rust.
|
|
35
|
+
RUST_ENCODE_IDENTICAL = {}.freeze
|
|
36
|
+
|
|
37
|
+
# usize::MAX — the FFI's "caller does not know the plaintext
|
|
38
|
+
# size" sentinel (streaming decode contract).
|
|
39
|
+
UNKNOWN_LENGTH = 0xFFFF_FFFF_FFFF_FFFF
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def for(codec, direction)
|
|
43
|
+
mode = ENV.fetch("OMNIZIP_BACKEND", "auto")
|
|
44
|
+
|
|
45
|
+
return backend_rust if mode == "rust"
|
|
46
|
+
return backend_ruby if mode == "ruby"
|
|
47
|
+
|
|
48
|
+
return backend_ruby unless Implementations::Rust::Library.available?
|
|
49
|
+
return backend_ruby unless Implementations::Rust::Library::CODECS.include?(codec)
|
|
50
|
+
|
|
51
|
+
direction == :encode && !RUST_ENCODE_IDENTICAL.fetch(codec, false) ? backend_ruby : backend_rust
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Compress through the tier-selected backend. The block is
|
|
55
|
+
# the pure-Ruby core — evaluated ONLY on the Ruby path, so
|
|
56
|
+
# the accelerated path never pays for it.
|
|
57
|
+
def compress(codec, data, level, &ruby_core)
|
|
58
|
+
backend = Backends.for(codec, :encode)
|
|
59
|
+
return yield if backend == RubyBackend
|
|
60
|
+
|
|
61
|
+
backend.compress(codec, data, level)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Decompress through the tier-selected backend (output-
|
|
65
|
+
# invariant: :auto prefers Rust whenever loadable).
|
|
66
|
+
def decompress(codec, data, expected_len, &ruby_core)
|
|
67
|
+
backend = Backends.for(codec, :decode)
|
|
68
|
+
return yield if backend == RubyBackend
|
|
69
|
+
|
|
70
|
+
backend.decompress(codec, data, expected_len)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def backend_rust
|
|
76
|
+
RustBackend
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def backend_ruby
|
|
80
|
+
RubyBackend
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The pure-Ruby path: a no-op backend — callers fall back to
|
|
85
|
+
# their own in-tree cores, which ARE the reference. Kept as an
|
|
86
|
+
# object for symmetry with the Rust side.
|
|
87
|
+
module RubyBackend
|
|
88
|
+
module_function
|
|
89
|
+
|
|
90
|
+
# :nocov:
|
|
91
|
+
def compress(codec, _data, _level)
|
|
92
|
+
raise Error, "#{codec}: ruby core not wired through Backends (call it directly)"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def decompress(codec, _data, _expected_len)
|
|
96
|
+
raise Error, "#{codec}: ruby core not wired through Backends (call it directly)"
|
|
97
|
+
end
|
|
98
|
+
# :nocov:
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The Rust-accelerated path over the Fiddle handle.
|
|
102
|
+
module RustBackend
|
|
103
|
+
module_function
|
|
104
|
+
|
|
105
|
+
def compress(codec, data, level)
|
|
106
|
+
Implementations::Rust::Library.instance.compress(codec, data, level)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def decompress(codec, data, expected_len)
|
|
110
|
+
Implementations::Rust::Library.instance.decompress(codec, data, expected_len)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class Error < StandardError; end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -80,7 +80,10 @@ module Omnizip
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
# Archive-vs-stream routing derives from the registry's
|
|
84
|
+
# extension routes so new formats route here automatically
|
|
85
|
+
ARCHIVE_EXTENSIONS = (Convenience::ARCHIVE_FORMAT_EXTENSIONS.keys +
|
|
86
|
+
Convenience::READ_ARCHIVE_FORMAT_EXTENSIONS.keys).freeze
|
|
84
87
|
|
|
85
88
|
private
|
|
86
89
|
|
|
@@ -199,6 +199,11 @@ module Omnizip
|
|
|
199
199
|
if @options[:solid] && Compression::Lzss.available?
|
|
200
200
|
write_solid_block(f, @files)
|
|
201
201
|
else
|
|
202
|
+
if @options[:solid]
|
|
203
|
+
warn "omnizip: solid: true requested but the " \
|
|
204
|
+
"RAR-compatible LZSS encoder is not available " \
|
|
205
|
+
"— writing independent STORE entries instead"
|
|
206
|
+
end
|
|
202
207
|
@files.each do |file|
|
|
203
208
|
write_file_entry(f, file)
|
|
204
209
|
end
|
|
@@ -152,6 +152,8 @@ module Omnizip
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
def write_precompressed_to_io(io, compression_method: COMPRESSION_DEFLATE)
|
|
155
|
+
check_classic_entry_count!
|
|
156
|
+
entries.each { |entry| check_classic_size_limit!(entry) }
|
|
155
157
|
local_header_offsets = []
|
|
156
158
|
|
|
157
159
|
entries.each do |entry|
|
|
@@ -189,6 +191,7 @@ module Omnizip
|
|
|
189
191
|
|
|
190
192
|
# Write to an IO object
|
|
191
193
|
def write_to_io(io, compression_method: COMPRESSION_DEFLATE, level: 6)
|
|
194
|
+
check_classic_entry_count!
|
|
192
195
|
local_header_offsets = []
|
|
193
196
|
|
|
194
197
|
# Write local file headers and data. An entry-level
|
|
@@ -220,6 +223,8 @@ module Omnizip
|
|
|
220
223
|
entry[:crc32] = calculate_crc32(entry[:uncompressed_data])
|
|
221
224
|
end
|
|
222
225
|
|
|
226
|
+
check_classic_size_limit!(entry)
|
|
227
|
+
|
|
223
228
|
# Update local header with compressed sizes
|
|
224
229
|
local_header.compressed_size = entry[:compressed_size]
|
|
225
230
|
local_header.uncompressed_size = entry[:uncompressed_size]
|
|
@@ -234,6 +239,7 @@ module Omnizip
|
|
|
234
239
|
|
|
235
240
|
# Record start of central directory
|
|
236
241
|
central_directory_offset = io.pos
|
|
242
|
+
check_classic_offset_limit!(central_directory_offset)
|
|
237
243
|
|
|
238
244
|
# Write central directory headers
|
|
239
245
|
entries.each_with_index do |entry, index|
|
|
@@ -257,8 +263,42 @@ module Omnizip
|
|
|
257
263
|
io.write(eocd.to_binary)
|
|
258
264
|
end
|
|
259
265
|
|
|
266
|
+
# Classic (non-ZIP64) ZIP headers pack sizes and offsets as
|
|
267
|
+
# 32-bit values and entry counts as 16-bit; exceeding them
|
|
268
|
+
# raises instead of letting Array#pack fail with a bare
|
|
269
|
+
# RangeError. ZIP64 writing is not supported.
|
|
270
|
+
MAX_CLASSIC_SIZE = 0xFFFFFFFF
|
|
271
|
+
MAX_CLASSIC_ENTRIES = 0xFFFF
|
|
272
|
+
private_constant :MAX_CLASSIC_SIZE, :MAX_CLASSIC_ENTRIES
|
|
273
|
+
|
|
260
274
|
private
|
|
261
275
|
|
|
276
|
+
def check_classic_entry_count!
|
|
277
|
+
return if entries.size <= MAX_CLASSIC_ENTRIES
|
|
278
|
+
|
|
279
|
+
raise Omnizip::UnsupportedFormatError,
|
|
280
|
+
"#{entries.size} entries exceed the classic ZIP " \
|
|
281
|
+
"limit of #{MAX_CLASSIC_ENTRIES} (ZIP64 writing is " \
|
|
282
|
+
"not supported)"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def check_classic_size_limit!(entry)
|
|
286
|
+
return if entry[:compressed_size].to_i <= MAX_CLASSIC_SIZE &&
|
|
287
|
+
entry[:uncompressed_size].to_i <= MAX_CLASSIC_SIZE
|
|
288
|
+
|
|
289
|
+
raise Omnizip::UnsupportedFormatError,
|
|
290
|
+
"entry #{entry[:filename].inspect} exceeds the 4 GiB " \
|
|
291
|
+
"classic ZIP size limit (ZIP64 writing is not supported)"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def check_classic_offset_limit!(offset)
|
|
295
|
+
return if offset <= MAX_CLASSIC_SIZE
|
|
296
|
+
|
|
297
|
+
raise Omnizip::UnsupportedFormatError,
|
|
298
|
+
"archive layout exceeds the 4 GiB classic ZIP " \
|
|
299
|
+
"offset limit (ZIP64 writing is not supported)"
|
|
300
|
+
end
|
|
301
|
+
|
|
262
302
|
# Create an entry hash
|
|
263
303
|
def create_entry(
|
|
264
304
|
filename:,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
# Ruby 4.0 moved fiddle out of the default gems.
|
|
5
|
+
require "fiddle"
|
|
6
|
+
require "pathname"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
# :nocov:
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Omnizip
|
|
12
|
+
module Implementations
|
|
13
|
+
module Rust
|
|
14
|
+
# The loaded cdylib: one handle, lazily opened, thread-safe
|
|
15
|
+
# after initialization (Fiddle::Function is immutable once
|
|
16
|
+
# built; compress/decompress are pure C calls).
|
|
17
|
+
class Library
|
|
18
|
+
# Codecs the cdylib dispatches by name.
|
|
19
|
+
CODECS = %w[bzip2 zstd lzma xz].freeze
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# The shared handle, or nil when the cdylib cannot load.
|
|
23
|
+
# Load failures are cached: a missing library is a stable
|
|
24
|
+
# condition, not something to re-attempt per call.
|
|
25
|
+
def instance
|
|
26
|
+
return @instance if defined?(@instance)
|
|
27
|
+
|
|
28
|
+
@instance = open
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def available?
|
|
32
|
+
!instance.nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Drop the cached handle so resolution reruns (test seam:
|
|
36
|
+
# specs exercising fallback/absence paths need a clean
|
|
37
|
+
# slate; production never calls this).
|
|
38
|
+
def forget!
|
|
39
|
+
@instance = nil if defined?(@instance)
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Built lazily: the Fiddle::TYPE_* constants only resolve
|
|
44
|
+
# after a successful require; environments without fiddle
|
|
45
|
+
# (Ruby 4.0 without the gem) never touch this table.
|
|
46
|
+
def function_table
|
|
47
|
+
{
|
|
48
|
+
last_error: ["ozip_last_error", [], Fiddle::TYPE_VOIDP],
|
|
49
|
+
free: ["ozip_free", %i[voidp size_t], Fiddle::TYPE_VOID],
|
|
50
|
+
compress: [
|
|
51
|
+
"ozip_compress",
|
|
52
|
+
%i[voidp voidp size_t int voidp],
|
|
53
|
+
Fiddle::TYPE_VOIDP,
|
|
54
|
+
],
|
|
55
|
+
decompress: [
|
|
56
|
+
"ozip_decompress",
|
|
57
|
+
%i[voidp voidp size_t size_t voidp],
|
|
58
|
+
Fiddle::TYPE_VOIDP,
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def open
|
|
66
|
+
path = resolve_path
|
|
67
|
+
return nil if path.nil?
|
|
68
|
+
|
|
69
|
+
handle = Fiddle.dlopen(path.to_s)
|
|
70
|
+
funcs = function_table.each_with_object({}) do |(key, (name, args, ret)), h|
|
|
71
|
+
h[key] = Fiddle::Function.new(handle[name], args, ret)
|
|
72
|
+
end
|
|
73
|
+
new(funcs)
|
|
74
|
+
rescue StandardError => e # includes Fiddle::DLError
|
|
75
|
+
warn "omnizip: rust backend unavailable (#{e.message}); using pure Ruby" if ENV["OMNIZIP_BACKEND"] == "rust"
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_path
|
|
80
|
+
explicit = ENV.fetch("OMNIZIP_FFI_DYLIB", nil)
|
|
81
|
+
return Pathname.new(explicit) if explicit && File.file?(explicit)
|
|
82
|
+
|
|
83
|
+
candidates = [
|
|
84
|
+
Pathname.new(__dir__).join("../../../../ext/libomnizip_ffi"),
|
|
85
|
+
sibling_checkout,
|
|
86
|
+
].compact.flatten
|
|
87
|
+
|
|
88
|
+
%w[.dylib .so .dll].each do |ext|
|
|
89
|
+
candidates.each do |dir|
|
|
90
|
+
path = dir.join("libomnizip_ffi#{ext}")
|
|
91
|
+
return path if path.file?
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A sibling omnizip-rs checkout: ../omnizip-rs relative to
|
|
98
|
+
# this gem's repository root.
|
|
99
|
+
def sibling_checkout
|
|
100
|
+
gem_root = Pathname.new(__dir__).join("../../../../..")
|
|
101
|
+
%w[omnizip-rs target/release].map do |rel|
|
|
102
|
+
gem_root.join(rel, "target/release")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def initialize(functions)
|
|
108
|
+
@functions = functions.freeze
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def compress(codec, data, level)
|
|
112
|
+
out_len = [0].pack("Q")
|
|
113
|
+
ptr = call(:compress, codec, data, data.bytesize, level, out_len)
|
|
114
|
+
take(ptr, out_len.unpack1("Q"))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def decompress(codec, data, expected_len)
|
|
118
|
+
out_len = [0].pack("Q")
|
|
119
|
+
ptr = call(:decompress, codec, data, data.bytesize, expected_len, out_len)
|
|
120
|
+
take(ptr, out_len.unpack1("Q"))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def last_error
|
|
124
|
+
ptr = @functions[:last_error].call
|
|
125
|
+
ptr.null? ? "unknown error" : ptr.to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def call(name, codec, input, input_len, a, out_len)
|
|
131
|
+
ptr = @functions[name].call(codec, input, input_len, a, out_len)
|
|
132
|
+
raise Error, last_error if ptr.null?
|
|
133
|
+
|
|
134
|
+
ptr
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Copy out + free in one breath: the buffer belongs to the
|
|
138
|
+
# Rust allocator and must never outlive ozip_free.
|
|
139
|
+
def take(ptr, len)
|
|
140
|
+
string = ptr.to_s(len)
|
|
141
|
+
@functions[:free].call(ptr, len)
|
|
142
|
+
string
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Raised when a codec call fails; #message carries the Rust
|
|
147
|
+
# side's last_error text.
|
|
148
|
+
class Error < StandardError; end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
module Implementations
|
|
5
|
+
# Rust-accelerated codec backend (TODO.ref-parity/62).
|
|
6
|
+
#
|
|
7
|
+
# Loads the omnizip-ffi cdylib through stdlib Fiddle — no
|
|
8
|
+
# compiled Ruby extension, no rake-compiler. When the library
|
|
9
|
+
# is unavailable the gem transparently stays on the pure-Ruby
|
|
10
|
+
# path; nothing about the fallback is an error.
|
|
11
|
+
#
|
|
12
|
+
# Library resolution order:
|
|
13
|
+
# 1. ENV['OMNIZIP_FFI_DYLIB'] (explicit path)
|
|
14
|
+
# 2. ../target/release/ inside a sibling omnizip-rs checkout
|
|
15
|
+
#
|
|
16
|
+
# Tier policy (see Omnizip::Backends): decode is always
|
|
17
|
+
# output-invariant, so `auto` uses Rust for decode whenever
|
|
18
|
+
# available; encode uses Rust only when byte-identity with the
|
|
19
|
+
# pure-Ruby encoder has been verified for that codec
|
|
20
|
+
# (RUST_ENCODE_IDENTICAL), keeping content addressing stable.
|
|
21
|
+
module Rust
|
|
22
|
+
autoload :Library, "omnizip/implementations/rust/library"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/omnizip/version.rb
CHANGED
data/lib/omnizip.rb
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
module Omnizip
|
|
20
20
|
autoload :VERSION, "omnizip/version"
|
|
21
|
+
autoload :Backends, "omnizip/backends"
|
|
21
22
|
|
|
22
23
|
autoload :Error, "omnizip/error"
|
|
23
24
|
autoload :CompressionError, "omnizip/error"
|
|
@@ -120,6 +121,7 @@ module Omnizip
|
|
|
120
121
|
module Implementations
|
|
121
122
|
autoload :SevenZip, "omnizip/implementations/seven_zip"
|
|
122
123
|
autoload :XZUtils, "omnizip/implementations/xz_utils"
|
|
124
|
+
autoload :Rust, "omnizip/implementations/rust"
|
|
123
125
|
|
|
124
126
|
module SevenZip
|
|
125
127
|
module LZMA
|
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
|
+
version: 0.4.0
|
|
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-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -334,6 +334,7 @@ files:
|
|
|
334
334
|
- lib/omnizip/archive_handlers/tar_handler.rb
|
|
335
335
|
- lib/omnizip/archive_handlers/xar_handler.rb
|
|
336
336
|
- lib/omnizip/archive_handlers/zip_handler.rb
|
|
337
|
+
- lib/omnizip/backends.rb
|
|
337
338
|
- lib/omnizip/buffer.rb
|
|
338
339
|
- lib/omnizip/buffer/memory_archive.rb
|
|
339
340
|
- lib/omnizip/buffer/memory_extractor.rb
|
|
@@ -598,6 +599,8 @@ files:
|
|
|
598
599
|
- lib/omnizip/implementations/base/lzma_decoder_base.rb
|
|
599
600
|
- lib/omnizip/implementations/base/lzma_encoder_base.rb
|
|
600
601
|
- lib/omnizip/implementations/base/state_machine_base.rb
|
|
602
|
+
- lib/omnizip/implementations/rust.rb
|
|
603
|
+
- lib/omnizip/implementations/rust/library.rb
|
|
601
604
|
- lib/omnizip/implementations/seven_zip.rb
|
|
602
605
|
- lib/omnizip/implementations/seven_zip/lzma.rb
|
|
603
606
|
- lib/omnizip/implementations/seven_zip/lzma/decoder.rb
|