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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ea5d1a7c799ac0f1b17c60e1c715fc0bf95a2d8dc3d3a02e0776bac6efbccf0
4
- data.tar.gz: 198583bbdc571acdd26a8a1e25f4a6ce0e5aa6b33d613c247bf49556d164200c
3
+ metadata.gz: 7a04b0c83ee4e9c6b57823c8db709178ed2b88e073f658fb6573d9f69fa7459f
4
+ data.tar.gz: 1c5d9b7e86c1d1322cfd8a8193fcec2c8b7a4d28a21b6ee4b6fca3a359bc01e3
5
5
  SHA512:
6
- metadata.gz: edaf17357204a214beecdd32e18bd53139bb6a7b9797b29a181a44db80b531e847f6d3e48da97f6ce5f02e80feeb456c71e593df92b471b4d5875a8d13ae1e98
7
- data.tar.gz: 6f82822274ad086eef57304f2386a43f979525dd6893f0eabf693b6c8c60a08a71729beb5c2a3328c8617871bbbc4b9d33a2b6801ae801eeb3614a8f4cbd6d4e
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 (v0.5.0)
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 Compression (v0.5.0)
113
+ === RAR5 Solid Archives
114
114
 
115
- Achieve 10-30% better compression ratios for similar files:
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
- compression: :lzma,
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
- # Best for:
131
- # - Source code repositories
132
- # - Log files
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 (v0.5.0)
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 (v0.5.0)
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 (v0.5.0)
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 Compression:**
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, # Best compression
271
- solid: true, # 10-30% better ratios
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
- output_stream.write(Bz2.compress(input_stream.read, level))
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
- output_stream.write(Bz2.decompress(input_stream.read))
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
- ARCHIVE_EXTENSIONS = %w[.zip .7z .tar .rar .cpio .iso .rpm .xar .msi].freeze
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.57"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.57
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-02 00:00:00.000000000 Z
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