omnizip 0.3.58 → 0.4.1

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: 04bb0d1c81ef7f4571014bb6679ad0d4750d4c078729daea82c88f643e246818
4
- data.tar.gz: 7523e6a77a43db5eb02ec7c16ef4adf0751150129f329af82eb11297f93a68f6
3
+ metadata.gz: 06cbc9a2cd22e74a84d82211efa5e000d857da117bae9a995b76520839211362
4
+ data.tar.gz: e8ab9ec5638953dda59f4b43aa299cfa55262f77d3c9e9f795bbe8800d443592
5
5
  SHA512:
6
- metadata.gz: da1c027f6e44246e6421760d3197501db38957aeb1541c0db6e3553ab73b5e03a221a362b0165d4196bf1185f0022b341529321cdce9c2a339c3cf2f31b4f733
7
- data.tar.gz: 7732ed6d8268d006195f7648980e2f8619ea89d45767932afd587f508592f627697a9cc436f6f3e1a01b41a8a579034f654c1bc33cc3331fced28af5bd68b8ff
6
+ metadata.gz: ecd9426b7a3f0a75c68821ec6c2c4c0f0f4f6f5e5ae7a252dc6e1e4fb9f5eb94a01b84f937c797581b44584e8cc4ba244d35e37c2338b1401c46de42299a1627
7
+ data.tar.gz: 268f70fb015d2e21b8079cae5ea4e09a83e036e967cf83bed03cc37de45b61f1875547b1443b419eec502a3bcc264e323d19bb8c503f72437cfb47bbd9d31853
data/CHANGELOG.md CHANGED
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+ - `rake rust:build` — one command to build and vendor the Rust
12
+ backend's cdylib (clones omnizip-rs or reuses a checkout via
13
+ REPO=); the loader gains the `vendor/` resolution tier behind
14
+ `OMNIZIP_FFI_DYLIB`. No toolchain, no failure: the gem stays
15
+ pure Ruby. docs/rust-backend.md documents the tier policy,
16
+ resolution order, and CI pinning.
17
+
18
+
19
+ ### Added
20
+ - Rust-accelerated backend tier: `Omnizip::Backends` is the single
21
+ tier switch between the pure-Ruby cores and the `omnizip-ffi`
22
+ cdylib (stdlib Fiddle — no compiled extension). Under the default
23
+ `auto` policy decode prefers the Rust decoders (output-invariant
24
+ and carrying fixes the Ruby cores lack); encode stays pure-Ruby
25
+ so frame bytes never depend on the tier (content addressing
26
+ stability). `OMNIZIP_BACKEND=rust|ruby` forces a tier; load
27
+ failures fall back to pure Ruby silently. A cross-tier
28
+ differential spec pins decode agreement in both encode
29
+ directions.
30
+ - BZip2 routes its byte core through the backend seam (both paths
31
+ share the algorithm class's IO plumbing).
32
+
33
+ ### Fixed
34
+ - Ruby 4.0 compatibility for the tier loader: fiddle moved out of
35
+ the default gems and is now required lazily with a fallback;
36
+ library resolution no longer references Fiddle constants when
37
+ fiddle is absent and considers `.dll` candidates on Windows.
38
+
39
+ ## [0.3.58] - 2026-09-03
40
+
41
+ ### Fixed
42
+ - `solid: true` on the RAR5 writer silently fell back to
43
+ independent STORE entries (the RAR-compatible LZSS encoder is not
44
+ available); the writer now warns about the fallback. The
45
+ creating-archives guide's solid section stops advertising
46
+ compression ratios the option cannot deliver and the stale
47
+ `(v0.5.0)` version-gate labels are gone from shipped features.
48
+ - The ZIP writer never emits ZIP64 structures; past the classic
49
+ limits `Array#pack` failed with a bare `RangeError` that named no
50
+ limit. Both write paths now raise `UnsupportedFormatError` naming
51
+ the 65,535-entry / 4 GiB size / 4 GiB offset classic-ZIP limits
52
+ (ZIP64 writing is not supported).
53
+
54
+ ### Changed
55
+ - The decompress command's archive-vs-stream routing derives from
56
+ the registry's extension routes instead of a private table, so
57
+ formats added to the registry route correctly instead of silently
58
+ falling into the single-file stream path.
59
+
10
60
  ## [0.3.57] - 2026-09-02
11
61
 
12
62
  ### Fixed
File without changes
@@ -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
@@ -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.58"
4
+ VERSION = "0.4.1"
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
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # One command to build the Rust-accelerated backend's cdylib into
4
+ # the gem's vendor dir (TODO.ref-parity/62 — the distribution
5
+ # story). Requires git + a Rust toolchain; the gem stays fully
6
+ # functional (pure Ruby) without ever running this.
7
+ #
8
+ # bundle exec rake rust:build
9
+ # bundle exec rake rust:build REPO=/path/to/omnizip-rs
10
+ desc "Build the omnizip-ffi cdylib into vendor/ for the Rust tier"
11
+ task "rust:build" do
12
+ repo = ENV.fetch("REPO", nil)
13
+ gem_root = File.expand_path("../..", __dir__)
14
+ build = File.join(gem_root, "build", "rust")
15
+
16
+ require "fileutils"
17
+ FileUtils.mkdir_p(build)
18
+ src = if repo
19
+ File.expand_path(repo)
20
+ else
21
+ target = File.join(build, "omnizip-rs")
22
+ if File.directory?(File.join(target, ".git"))
23
+ system("git", "-C", target, "fetch", "--quiet", "origin") or abort "git fetch failed in #{target}"
24
+ system("git", "-C", target, "reset", "--quiet", "--hard", "origin/main") or abort "git reset failed"
25
+ else
26
+ system("git", "clone", "--quiet", "--depth", "1",
27
+ "https://github.com/omnizip/omnizip-rs.git", target) or abort "clone failed"
28
+ end
29
+ target
30
+ end
31
+
32
+ abort "#{src} has no omnizip-ffi crate" unless File.directory?(File.join(src, "omnizip-ffi"))
33
+
34
+ system("cargo", "build", "--release", "-p", "omnizip-ffi", chdir: src) or abort "cargo build failed"
35
+
36
+ vendor = File.join(gem_root, "vendor")
37
+ FileUtils.mkdir_p(vendor)
38
+ %w[dylib so dll].each do |ext|
39
+ candidate = File.join(src, "target", "release", "libomnizip_ffi.#{ext}")
40
+ next unless File.file?(candidate)
41
+
42
+ FileUtils.cp(candidate, vendor)
43
+ puts "vendored: #{File.join(vendor, "libomnizip_ffi.#{ext}")}"
44
+ puts "tier: OMNIZIP_BACKEND=auto now uses the Rust decoders"
45
+ exit 0
46
+ end
47
+ abort "cdylib not found under #{src}/target/release"
48
+ 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.3.58
4
+ version: 0.4.1
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-03 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
@@ -211,6 +211,7 @@ files:
211
211
  - docs/reference/index.adoc
212
212
  - docs/resources/faq.adoc
213
213
  - docs/resources/quick-reference.adoc
214
+ - docs/rust-backend.md
214
215
  - docs/troubleshooting/index.adoc
215
216
  - docs/xar_format.md
216
217
  - examples/api_comparison.rb
@@ -334,6 +335,7 @@ files:
334
335
  - lib/omnizip/archive_handlers/tar_handler.rb
335
336
  - lib/omnizip/archive_handlers/xar_handler.rb
336
337
  - lib/omnizip/archive_handlers/zip_handler.rb
338
+ - lib/omnizip/backends.rb
337
339
  - lib/omnizip/buffer.rb
338
340
  - lib/omnizip/buffer/memory_archive.rb
339
341
  - lib/omnizip/buffer/memory_extractor.rb
@@ -598,6 +600,8 @@ files:
598
600
  - lib/omnizip/implementations/base/lzma_decoder_base.rb
599
601
  - lib/omnizip/implementations/base/lzma_encoder_base.rb
600
602
  - lib/omnizip/implementations/base/state_machine_base.rb
603
+ - lib/omnizip/implementations/rust.rb
604
+ - lib/omnizip/implementations/rust/library.rb
601
605
  - lib/omnizip/implementations/seven_zip.rb
602
606
  - lib/omnizip/implementations/seven_zip/lzma.rb
603
607
  - lib/omnizip/implementations/seven_zip/lzma/decoder.rb
@@ -719,6 +723,7 @@ files:
719
723
  - lib/omnizip/zip/input_stream.rb
720
724
  - lib/omnizip/zip/output_stream.rb
721
725
  - lib/tasks/msi_test_files.rake
726
+ - lib/tasks/rust_backend.rake
722
727
  - readme-docs/advanced-features.adoc
723
728
  - readme-docs/api-usage.adoc
724
729
  - readme-docs/architecture.adoc