omnizip 0.3.21 → 0.3.23

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: 5d0052490f16345b27296100499706bddd9936ea4a4b839dc34ea862bef32d77
4
- data.tar.gz: f33f511d967b2a4f5d881b0e9205c72e57a9744480fdcb914b5ee44df9a14983
3
+ metadata.gz: 61cf26f9e324600f286b236ab80e02d720059f32bca2fa3c71640ba3abc4355a
4
+ data.tar.gz: fa189e081e402ecb4d084490e7874402faacf231a296cd009a15f218fcd9453e
5
5
  SHA512:
6
- metadata.gz: 722b0ece40f17d907a74df2fd9a333945f5060101f6c816bc613c56b1e653d907b31fc803afe40b840217e70dd6301304e0c39660071a0fab20d3df4e6a9c884
7
- data.tar.gz: f3810c7f1d35b12a88dbc2651945ff18d66399b42f5bb3db1299aece2545fceb9e6336025d2e3555d4b8aa4608f3991091ddd33dca0d575a1b6153da0b4c6a02
6
+ metadata.gz: 308660d6095a909d72c528bdfb5e828e76d320172b979a53ef7401aa2c1c12a560a0afd57f49991eb5190f87deca1c15cd798e8abae89bd3341ed3bb43c39b3d
7
+ data.tar.gz: 29ac9066539f2d8cef3973a2eb9cfe15a549a5efead04a1f5c88f28587fb6a2d23ac28c401c1c4187f5c226bfe4b531e3565523400dcd183adc83c842cd2d5dc
data/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.23] - 2026-08-26
11
+
12
+ ### Changed
13
+ - Zstd encoder hot paths use allocation-free integer reads instead of
14
+ `String#byteslice` + `unpack1` comparisons (profiling showed GC at
15
+ 52% of encoder time): default-level compression is ~3.4x faster and
16
+ level 22 ~1.9x on the benchmark corpus, with byte-identical output.
17
+
18
+ ## [0.3.22] - 2026-08-25
19
+
20
+ ### Added
21
+ - Zstandard dictionary compression (`Dictionary.from_raw` /
22
+ `serialize` / `deserialize`, `compress_with_dict`,
23
+ `decompress_with_dict`): the dictionary content primes the match
24
+ finder as shared history and the frame header carries the
25
+ Dictionary_ID, which the decoder verifies (Phase-1 scope, as in the
26
+ Rust reference; entropy-table preloading is future work).
27
+
28
+ ## [0.3.21] - 2026-08-25
29
+
30
+ ### Fixed
31
+ - 7-Zip encoder dictionary is now capped at the size announced in the
32
+ coder properties (prevents matches reading outside the decoder
33
+ window for inputs above the announced size).
34
+
35
+ ### Changed
36
+ - Zstd lazy levels (6+) gained the rep0 fast-path and backward
37
+ extension: levels 6-12 improve 0.180 -> 0.154 and levels 19-22
38
+ 0.146 -> 0.126 on the benchmark corpus; the level scale is now
39
+ monotonic.
40
+ - Zstd adaptive block splitting: heterogeneous chunks of 32 KiB or
41
+ more split into 16 KiB sub-blocks so entropy tables fit each
42
+ content regime.
43
+
44
+ ## [0.3.20] - 2026-08-25
45
+
46
+ ### Removed
47
+ - Dead `SevenZipLZMA2`/`XZLZMA2` algorithm wrappers (never
48
+ autoloaded; one referenced a nonexistent decoder constant).
49
+
10
50
  ## [0.3.19] - 2026-08-25
11
51
 
12
52
  ### Fixed
@@ -79,6 +79,35 @@ module Omnizip
79
79
  output
80
80
  end
81
81
 
82
+ # Decode a stream produced by a dictionary-primed encoder: the
83
+ # dictionary's content primes the reconstruction window of
84
+ # frames carrying its ID (verified; mismatch raises).
85
+ #
86
+ # @param dict [Dictionary]
87
+ # @return [String] decompressed data (binary)
88
+ def decode_stream_with_dict(dict)
89
+ data = read_all
90
+ output = String.new(encoding: Encoding::BINARY)
91
+ pos = 0
92
+
93
+ loop do
94
+ remaining = data.bytesize - pos
95
+ break if remaining.zero?
96
+ raise Omnizip::DecompressionError, "trailing bytes are not a frame" if remaining < 4
97
+
98
+ magic = data.byteslice(pos, 4).unpack1("V")
99
+ unless magic == MAGIC_NUMBER
100
+ raise Omnizip::DecompressionError,
101
+ "invalid Zstandard magic: 0x#{magic.to_s(16)}"
102
+ end
103
+
104
+ frame_output, pos = decode_frame(data, pos + 4, dict)
105
+ output << frame_output
106
+ end
107
+
108
+ output
109
+ end
110
+
82
111
  private
83
112
 
84
113
  def read_all
@@ -101,8 +130,15 @@ module Omnizip
101
130
 
102
131
  # Decode one frame (input positioned after the magic).
103
132
  # Returns [frame_output, pos_after_frame].
104
- def decode_frame(data, pos)
133
+ #
134
+ # With a dictionary prefix, the reconstruction window is
135
+ # primed with the dictionary content so sequences can
136
+ # back-reference it; the prefix is verified (checksum) and
137
+ # stripped from the returned output.
138
+ def decode_frame(data, pos, dict = nil)
105
139
  header, pos = Frame::Header.parse_from(data, pos)
140
+ verify_dictionary_id(header, dict)
141
+ prefix = dict&.content
106
142
 
107
143
  # Reset per-frame state: repeat offsets, previous Huffman
108
144
  # table, previous FSE tables.
@@ -111,6 +147,7 @@ module Omnizip
111
147
  @previous_fse_tables = {}
112
148
 
113
149
  output = String.new(encoding: Encoding::BINARY)
150
+ output << prefix if prefix
114
151
  loop do
115
152
  block, pos = Frame::Block.parse_from(data, pos)
116
153
  if block.reserved?
@@ -128,7 +165,9 @@ module Omnizip
128
165
  end
129
166
 
130
167
  expected = data.byteslice(pos, 4).unpack1("V")
131
- actual = XXHash64.frame_checksum(output)
168
+ actual = XXHash64.frame_checksum(
169
+ prefix ? output.byteslice(prefix.bytesize..) : output,
170
+ )
132
171
  if expected != actual
133
172
  raise Omnizip::DecompressionError,
134
173
  "frame checksum mismatch: stored 0x#{expected.to_s(16)}, " \
@@ -137,9 +176,29 @@ module Omnizip
137
176
  pos += 4
138
177
  end
139
178
 
140
- [output, pos]
179
+ frame_output = prefix ? output.byteslice(prefix.bytesize..) : output
180
+ [frame_output, pos]
181
+ end
182
+
183
+ # A frame that carries a Dictionary_ID must be decoded with a
184
+ # dictionary of the same ID; a mismatched or missing
185
+ # dictionary would silently garble every back-reference.
186
+ def verify_dictionary_id(header, dict)
187
+ return unless header.dictionary_id?
188
+
189
+ frame_id = header.dictionary_id
190
+ if dict.nil?
191
+ raise Omnizip::DecompressionError,
192
+ "frame requires dictionary #{frame_id}, none supplied"
193
+ end
194
+ return if dict.id == frame_id
195
+
196
+ raise Omnizip::DecompressionError,
197
+ "frame requires dictionary #{frame_id}, got #{dict.id}"
141
198
  end
142
199
 
200
+ # Decode one block into `output`. Returns the position after
201
+ # the block's payload.
143
202
  def decode_block(block, data, pos, output)
144
203
  case block.block_type
145
204
  when BLOCK_TYPE_RAW
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (C) 2025 Ribose Inc.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a
6
+ # copy of this software and associated documentation files (the "Software"),
7
+ # to deal in the Software without restriction, including without limitation
8
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
9
+ # and/or sell copies of the Software, and to permit persons to whom the
10
+ # Software is furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
+ # DEALINGS IN THE SOFTWARE.
22
+
23
+ module Omnizip
24
+ module Algorithms
25
+ class Zstandard
26
+ # Zstandard dictionary (port of the omnizip-rs dict.rs).
27
+ #
28
+ # A dictionary lets the encoder preload a reference-content
29
+ # window so small inputs compress dramatically better: the
30
+ # dictionary content is used as a match-finder prefix, and the
31
+ # frame header carries the dictionary's ID.
32
+ #
33
+ # Wire format (simplified form, as in the Rust reference):
34
+ # magic (4) + dictionary ID (4, LE) + raw content.
35
+ class Dictionary
36
+ DICT_MAGIC = 0xEC30A437
37
+
38
+ attr_reader :id, :content
39
+
40
+ # @param id [Integer] dictionary ID carried in frame headers
41
+ # @param content [String] corpus bytes used as the prefix
42
+ def initialize(id, content)
43
+ @id = id
44
+ @content = content.dup.force_encoding(Encoding::BINARY)
45
+ end
46
+
47
+ def self.from_raw(id, content)
48
+ new(id, content)
49
+ end
50
+
51
+ def self.deserialize(data)
52
+ if data.bytesize < 8
53
+ raise Omnizip::DecompressionError,
54
+ "dictionary too short for magic + id"
55
+ end
56
+ if data.byteslice(0, 4).unpack1("V") != DICT_MAGIC
57
+ raise Omnizip::DecompressionError, "bad dictionary magic"
58
+ end
59
+
60
+ new(data.byteslice(4, 4).unpack1("V"), data.byteslice(8..))
61
+ end
62
+
63
+ def serialize
64
+ [DICT_MAGIC].pack("V") + [@id].pack("V") + @content
65
+ end
66
+
67
+ def ==(other)
68
+ other.is_a?(Dictionary) && id == other.id && content == other.content
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -58,42 +58,83 @@ module Omnizip
58
58
  write_frame(data)
59
59
  end
60
60
 
61
+ # Encode a data stream primed with a dictionary prefix (port
62
+ # of the Rust encode_frame_with_dict): the dictionary content
63
+ # is prepended as virtual history, the match finder is seeded
64
+ # with its positions, and only the plaintext is emitted.
65
+ # Frame_Content_Size and the checksum cover the plaintext
66
+ # only; the frame header carries the dictionary's ID and
67
+ # requires the dict-aware decoder.
68
+ #
69
+ # @param data [String]
70
+ # @param dict [Dictionary]
71
+ def encode_stream_with_dict(data, dict)
72
+ data = data.dup.force_encoding(Encoding::BINARY)
73
+ virtual = dict.content + data
74
+ prefix_len = dict.content.bytesize
75
+
76
+ write_u32le(MAGIC_NUMBER)
77
+ write_frame_header(data, dict.id)
78
+ write_blocks(virtual, prefix_len)
79
+ write_u32le(XXHash64.frame_checksum(data)) if @checksum
80
+ end
81
+
61
82
  private
62
83
 
63
- def write_frame(data)
84
+ def write_frame(data, dict = nil)
64
85
  write_u32le(MAGIC_NUMBER)
65
- write_frame_header(data)
86
+ write_frame_header(data, dict && dict.id)
66
87
  write_blocks(data)
67
88
  write_u32le(XXHash64.frame_checksum(data)) if @checksum
68
89
  end
69
90
 
70
- # Single-segment frame header: no window descriptor, no
71
- # dictionary; Frame_Content_Size in 1, 2, 4 or 8 bytes.
72
- def write_frame_header(data)
91
+ # Single-segment frame header: no window descriptor;
92
+ # optional Dictionary_ID, then Frame_Content_Size in 1, 2, 4
93
+ # or 8 bytes.
94
+ def write_frame_header(data, dict_id = nil)
95
+ did_flag, did_bytes = dict_id_encoding(dict_id)
96
+
73
97
  size = data.bytesize
74
98
  if size < 256
75
- @output_stream.putc(0x20) # single segment, FCS 1 byte
99
+ @output_stream.putc(0x20 | did_flag) # single segment, FCS 1 byte
100
+ write_dict_id_bytes(did_bytes)
76
101
  @output_stream.putc(size)
77
102
  elsif size <= 65_791
78
- @output_stream.putc(0x60) # single segment, FCS 2 bytes (+256)
103
+ @output_stream.putc(0x60 | did_flag) # single segment, FCS 2 bytes
104
+ write_dict_id_bytes(did_bytes)
79
105
  write_u16le(size - 256)
80
106
  elsif size <= 0xFFFFFFFF
81
- @output_stream.putc(0xA0) # single segment, FCS 4 bytes
107
+ @output_stream.putc(0xA0 | did_flag) # single segment, FCS 4 bytes
108
+ write_dict_id_bytes(did_bytes)
82
109
  write_u32le(size)
83
110
  else
84
- @output_stream.putc(0xE0) # single segment, FCS 8 bytes
111
+ @output_stream.putc(0xE0 | did_flag) # single segment, FCS 8 bytes
112
+ write_dict_id_bytes(did_bytes)
85
113
  write_u64le(size)
86
114
  end
87
115
  end
88
116
 
117
+ def dict_id_encoding(dict_id)
118
+ return [0, "".b] if dict_id.nil? || dict_id.zero?
119
+ return [1, [dict_id].pack("C")] if dict_id <= 0xFF
120
+ return [2, [dict_id].pack("v")] if dict_id <= 0xFFFF
121
+
122
+ [3, [dict_id].pack("V")]
123
+ end
124
+
125
+ def write_dict_id_bytes(bytes)
126
+ @output_stream.write(bytes)
127
+ end
128
+
89
129
  # rubocop:disable Metrics/MethodLength
90
130
  # rubocop:disable-next Metrics/AbcSize
91
- def write_blocks(data)
92
- return write_empty_last_block if data.empty?
131
+ def write_blocks(data, start = 0)
132
+ return write_empty_last_block if data.bytesize <= start
93
133
 
94
134
  params = MatchFinder.params_for_level(@level, data.bytesize)
95
135
  ms = MatchFinder::MatchState.new(params[:hash_log])
96
136
  ms.enable_chain(params[:chain]) if params[:chain].positive?
137
+ ms.seed_prefix(data, start) if start.positive?
97
138
 
98
139
  # LDM (long-distance matching) at high levels on multi-block
99
140
  # inputs: a sparse table over the whole frame finds matches
@@ -115,7 +156,7 @@ module Omnizip
115
156
  # untouched.
116
157
  reps = [1, 4, 8]
117
158
 
118
- offset = 0
159
+ offset = start
119
160
  while offset < data.bytesize
120
161
  block_end = [offset + BLOCK_MAX_SIZE, data.bytesize].min
121
162
  is_last = block_end == data.bytesize
@@ -88,6 +88,18 @@ module Omnizip
88
88
  def disable_chain
89
89
  @max_chain = 0
90
90
  end
91
+
92
+ # Seed the hash table with a prefix's positions (dictionary
93
+ # content prepended to the plaintext): the most recent
94
+ # position per hash wins, matching insert order.
95
+ def seed_prefix(src, prefix_len)
96
+ return if prefix_len < MIN_MATCH
97
+
98
+ limit = prefix_len - MIN_MATCH + 1
99
+ (0...limit).each do |pos|
100
+ @hash_table[MatchFinder.hash4(src, pos, @hash_log)] = pos
101
+ end
102
+ end
91
103
  end
92
104
 
93
105
  module_function
@@ -119,11 +131,29 @@ module Omnizip
119
131
  min_match: MIN_MATCH_ECONOMICAL }
120
132
  end
121
133
 
122
- def hash4(src, pos, h_bits)
123
- v = src.getbyte(pos) |
134
+ # Allocation-free little-endian reads; the hot loops call
135
+ # these millions of times, and String#byteslice + unpack1
136
+ # showed up as half the encoder's time via GC pressure.
137
+ def read4(src, pos)
138
+ src.getbyte(pos) |
124
139
  (src.getbyte(pos + 1) << 8) |
125
140
  (src.getbyte(pos + 2) << 16) |
126
141
  (src.getbyte(pos + 3) << 24)
142
+ end
143
+
144
+ def read8(src, pos)
145
+ src.getbyte(pos) |
146
+ (src.getbyte(pos + 1) << 8) |
147
+ (src.getbyte(pos + 2) << 16) |
148
+ (src.getbyte(pos + 3) << 24) |
149
+ (src.getbyte(pos + 4) << 32) |
150
+ (src.getbyte(pos + 5) << 40) |
151
+ (src.getbyte(pos + 6) << 48) |
152
+ (src.getbyte(pos + 7) << 56)
153
+ end
154
+
155
+ def hash4(src, pos, h_bits)
156
+ v = read4(src, pos)
127
157
  # 32-bit wrapping multiply (C/uint32_t), then the high bits.
128
158
  ((v * PRIME4_BYTES) & 0xFFFFFFFF) >> (32 - h_bits)
129
159
  end
@@ -136,8 +166,8 @@ module Omnizip
136
166
  b_size = b.bytesize
137
167
  while len + 8 <= limit && a_pos + len + 8 <= a_size &&
138
168
  b_pos + len + 8 <= b_size
139
- wa = a.byteslice(a_pos + len, 8).unpack1("Q<")
140
- wb = b.byteslice(b_pos + len, 8).unpack1("Q<")
169
+ wa = read8(a, a_pos + len)
170
+ wb = read8(b, b_pos + len)
141
171
  if wa == wb
142
172
  len += 8
143
173
  else
@@ -277,8 +307,7 @@ module Omnizip
277
307
  # rubocop:disable-next Metrics/AbcSize
278
308
  def rep0_match(src, ip, rep0, min_match, limit, anchor)
279
309
  return nil if rep0 <= 0 || ip <= rep0
280
- return nil if src.byteslice(ip, MIN_MATCH) !=
281
- src.byteslice(ip - rep0, MIN_MATCH)
310
+ return nil if read4(src, ip) != read4(src, ip - rep0)
282
311
 
283
312
  m_len = MIN_MATCH + count_match(
284
313
  src, ip + MIN_MATCH, src, ip + MIN_MATCH - rep0,
@@ -334,7 +363,7 @@ module Omnizip
334
363
  break if dist >= max_distance
335
364
  break if candidate + MIN_MATCH > size
336
365
 
337
- if src.byteslice(ip, MIN_MATCH) == src.byteslice(candidate, MIN_MATCH)
366
+ if read4(src, ip) == read4(src, candidate)
338
367
  m_len = MIN_MATCH + count_match(src, ip + MIN_MATCH,
339
368
  src, candidate + MIN_MATCH,
340
369
  [max_extend - MIN_MATCH, 0].max)
@@ -385,7 +414,7 @@ module Omnizip
385
414
  if candidate.positive? && candidate < ip
386
415
  dist = ip - candidate
387
416
  if dist < max_distance && candidate + MIN_MATCH <= size &&
388
- src.byteslice(ip, MIN_MATCH) == src.byteslice(candidate, MIN_MATCH)
417
+ read4(src, ip) == read4(src, candidate)
389
418
  best_len = MIN_MATCH + count_match(
390
419
  src, ip + MIN_MATCH, src, candidate + MIN_MATCH,
391
420
  [limit + MIN_MATCH_ECONOMICAL - ip - MIN_MATCH, 0].max
@@ -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
@@ -58,6 +60,7 @@ module Omnizip
58
60
  "omnizip/algorithms/zstandard/sequences_encoder"
59
61
  autoload :MatchFinder, "omnizip/algorithms/zstandard/match_finder"
60
62
  autoload :LdmHashTable, "omnizip/algorithms/zstandard/ldm"
63
+ autoload :Dictionary, "omnizip/algorithms/zstandard/dictionary"
61
64
  autoload :XXHash64, "omnizip/algorithms/zstandard/xxhash"
62
65
 
63
66
  # Frame and FSE modules
@@ -101,6 +104,54 @@ module Omnizip
101
104
  output_stream.write(decompressed)
102
105
  end
103
106
 
107
+ # Compress data primed with a dictionary prefix: the dictionary
108
+ # content acts as shared history the match finder can reference,
109
+ # dramatically improving ratios on small inputs. The resulting
110
+ # frame carries the dictionary's ID and requires
111
+ # decompress_with_dict to decode.
112
+ #
113
+ # @param input_stream [IO] plaintext
114
+ # @param output_stream [IO] compressed frame
115
+ # @param dict [Dictionary]
116
+ # @param options [Hash] :level, :checksum
117
+ # @return [void]
118
+ def compress_with_dict(input_stream, output_stream, dict, options = {})
119
+ encoder = Encoder.new(output_stream, build_encoder_options(options))
120
+ encoder.encode_stream_with_dict(input_stream.read, dict)
121
+ end
122
+
123
+ # Decompress a frame produced by compress_with_dict. The frame's
124
+ # dictionary ID is verified against `dict` before decoding.
125
+ #
126
+ # @param input_stream [IO] compressed frame
127
+ # @param output_stream [IO] plaintext
128
+ # @param dict [Dictionary]
129
+ # @return [void]
130
+ def decompress_with_dict(input_stream, output_stream, dict)
131
+ output_stream.set_encoding(Encoding::BINARY)
132
+ decoder = Decoder.new(input_stream)
133
+ output_stream.write(decoder.decode_stream_with_dict(dict))
134
+ end
135
+
136
+ # Class-level convenience mirrors of the dictionary API.
137
+ #
138
+ # @return [String] compressed frame / decompressed plaintext
139
+ def self.compress_with_dict(data, dict, **options)
140
+ output = StringIO.new
141
+ output.set_encoding(Encoding::BINARY)
142
+ new(options).compress_with_dict(
143
+ StringIO.new(data.to_s.b), output, dict, options
144
+ )
145
+ output.string
146
+ end
147
+
148
+ def self.decompress_with_dict(data, dict)
149
+ output = StringIO.new
150
+ output.set_encoding(Encoding::BINARY)
151
+ new.decompress_with_dict(StringIO.new(data.to_s.b), output, dict)
152
+ output.string
153
+ end
154
+
104
155
  private
105
156
 
106
157
  # Build encoder options from compression options
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.21"
4
+ VERSION = "0.3.23"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnizip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.21
4
+ version: 0.3.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -301,6 +301,7 @@ files:
301
301
  - lib/omnizip/algorithms/zstandard.rb
302
302
  - lib/omnizip/algorithms/zstandard/constants.rb
303
303
  - lib/omnizip/algorithms/zstandard/decoder.rb
304
+ - lib/omnizip/algorithms/zstandard/dictionary.rb
304
305
  - lib/omnizip/algorithms/zstandard/encoder.rb
305
306
  - lib/omnizip/algorithms/zstandard/frame.rb
306
307
  - lib/omnizip/algorithms/zstandard/frame/block.rb