omnizip 0.3.16 → 0.3.18

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: 4417d9e90649e837f75b25bcee26c0721283e360d015eaf0c4194fa2957be053
4
- data.tar.gz: 8c5fc8e793ea023d21abd9b044c3ddfb3786ee90225fe57fb322fa8cc60c5f4a
3
+ metadata.gz: 689a5e44cef4327034541bbcdca28c15e75da2d6cb2452a0617365e107bf1597
4
+ data.tar.gz: d9bd5f70930e7efdb4fcaaba779927c0b0898e163e79e8e059a990ccc4083c31
5
5
  SHA512:
6
- metadata.gz: 2de569d977b1b47bc476efd9eb17e8f00c5b803e96451592964ee0f43bfa04caf52fd6462d02fbcaf9bd914b32436a210b1b935d24d6c1ad96a61acf119c7ca4
7
- data.tar.gz: c070c98a67fef377cf8710f89ed922bb0a6ff5cf8edd0ac7307113f4973d07a85c5eb8dfce48210f7d570687d361f68cc61f947d4dc739588f9aee1662126269
6
+ metadata.gz: c2ec5fa49e00586c54e7a267de4da9fdcccacd0545e664e81608a5a697ad5430e95143ffd08afa0bafc41b44e17073d0780d694330748e7e35949991a77c9643
7
+ data.tar.gz: f79484e4aaa748152c1ea5abca31a08adcfe22207037608e3714e6fac57222c2d65763c16a0b26ef434a9f22ba8122ce4f27cddf9e5d4dc8e0db0c1b9abfe020
@@ -85,6 +85,10 @@ module Omnizip
85
85
  MAX_LEVEL = 22
86
86
  DEFAULT_LEVEL = 3
87
87
 
88
+ # Long-distance matching kicks in at this level for inputs
89
+ # larger than one block (Btultra2 in the C strategy table).
90
+ LDM_MIN_LEVEL = 19
91
+
88
92
  # Buffer size for streaming operations
89
93
  BUFFER_SIZE = 128 * 1024 # 128KB
90
94
 
@@ -95,6 +95,21 @@ module Omnizip
95
95
  ms = MatchFinder::MatchState.new(params[:hash_log])
96
96
  ms.enable_chain(params[:chain]) if params[:chain].positive?
97
97
 
98
+ # LDM (long-distance matching) at high levels on multi-block
99
+ # inputs: a sparse table over the whole frame finds matches
100
+ # beyond the 128 KiB block window. Chains are disabled —
101
+ # the chain table is block-sized and LDM provides the
102
+ # long-distance coverage instead (Rust reference behavior).
103
+ ldm = nil
104
+ max_distance = BLOCK_MAX_SIZE
105
+ if @level >= LDM_MIN_LEVEL && data.bytesize > BLOCK_MAX_SIZE
106
+ window_log = data.bytesize.bit_length.clamp(10, 31)
107
+ ldm = LdmHashTable.new(data.bytesize, window_log)
108
+ (0...data.bytesize).each { |pos| ldm.insert(data, pos) }
109
+ ms.disable_chain
110
+ max_distance = data.bytesize
111
+ end
112
+
98
113
  # Wire repeat-offset state carried across blocks; matches the
99
114
  # decoder's executor state. Raw and RLE blocks leave it
100
115
  # untouched.
@@ -114,7 +129,7 @@ module Omnizip
114
129
  seq_store = MatchFinder::SeqStore.new(reps.dup)
115
130
  MatchFinder.compress_range(data, offset, block_end, seq_store,
116
131
  ms, params[:min_match],
117
- params[:lazy])
132
+ params[:lazy], ldm, max_distance)
118
133
  content, new_reps = try_compressed(seq_store, reps)
119
134
  if content.nil?
120
135
  write_block_header(is_last ? 1 : 0, BLOCK_TYPE_RAW,
@@ -0,0 +1,121 @@
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
+ # Long-Distance Matching sparse hash table (port of the
27
+ # omnizip-rs encoder/ldm.rs).
28
+ #
29
+ # Hashes every `gap`-th position into a head/chain table so the
30
+ # parser can find matches at distances far beyond the normal
31
+ # match-finder window — up to the full frame size. The table is
32
+ # pre-populated over the whole input before parsing starts, so
33
+ # entries may point forward of the current position; find_match
34
+ # skips those without spending its chain budget.
35
+ class LdmHashTable
36
+ # Chain entries walked per find_match (Rust LDM_MAX_CHAIN).
37
+ LDM_MAX_CHAIN = 32
38
+ MIN_MATCH = MatchFinder::MIN_MATCH
39
+
40
+ # Hash table and chain sizes are additionally capped by the
41
+ # number of sparse samples so small inputs do not pay for a
42
+ # window-sized table.
43
+ def initialize(input_len, window_log, gap = 16)
44
+ samples = (input_len / gap) + 1
45
+ hash_log = [[window_log, 21].min, samples.bit_length + 2].min
46
+ hash_log = 1 if hash_log < 1
47
+
48
+ @gap = gap
49
+ @hash_log = hash_log
50
+ @head = Array.new(1 << hash_log)
51
+ @chain = Array.new(samples)
52
+ end
53
+
54
+ # Insert `pos` into the table (sparse sampling: every gap-th
55
+ # position only).
56
+ def insert(src, pos)
57
+ return unless (pos % @gap).zero?
58
+
59
+ h = hash4(src, pos)
60
+ idx = pos / @gap
61
+ @chain[idx] = @head[h] if idx < @chain.length
62
+ @head[h] = pos
63
+ end
64
+
65
+ # Longest match at `pos` within `max_distance`, bounded by
66
+ # `end_bound` (the current block end). Walks up to MAX_CHAIN
67
+ # chain entries. Returns [distance, length] or nil.
68
+ #
69
+ # rubocop:disable Metrics/MethodLength
70
+ # rubocop:disable Metrics/AbcSize
71
+ # rubocop:disable-next Metrics/CyclomaticComplexity
72
+ def find_match(src, pos, max_distance, min_match, end_bound)
73
+ return nil if pos + MIN_MATCH > src.bytesize
74
+
75
+ candidate = @head[hash4(src, pos)]
76
+ best_len = 0
77
+ best_dist = 0
78
+ chain_count = 0
79
+
80
+ while candidate && chain_count < LDM_MAX_CHAIN
81
+ if candidate >= pos
82
+ candidate = @chain[candidate / @gap]
83
+ next
84
+ end
85
+
86
+ dist = pos - candidate
87
+ break if dist > max_distance
88
+
89
+ len_cap = [end_bound - pos, pos - candidate].min
90
+ len_cap = 0 if len_cap.negative?
91
+ len = MatchFinder.count_match(src, pos, src, candidate, len_cap)
92
+ if len > best_len && len >= min_match
93
+ best_len = len
94
+ best_dist = dist
95
+ end
96
+
97
+ candidate = @chain[candidate / @gap]
98
+ chain_count += 1
99
+ end
100
+
101
+ return nil unless best_len >= min_match && best_dist.positive?
102
+
103
+ [best_dist, best_len]
104
+ end
105
+ # rubocop:enable Metrics/AbcSize
106
+ # rubocop:enable Metrics/MethodLength
107
+
108
+ private
109
+
110
+ # rubocop:disable-next Metrics/AbcSize
111
+ def hash4(src, pos)
112
+ v = src.getbyte(pos) |
113
+ (src.getbyte(pos + 1) << 8) |
114
+ (src.getbyte(pos + 2) << 16) |
115
+ (src.getbyte(pos + 3) << 24)
116
+ ((v * MatchFinder::PRIME4_BYTES) & 0xFFFFFFFF) >> (32 - @hash_log)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -163,7 +163,8 @@ module Omnizip
163
163
  # rubocop:disable Metrics/CyclomaticComplexity
164
164
  # rubocop:disable-next Metrics/PerceivedComplexity
165
165
  def compress_range(src, block_start, block_end, seq_store, ms,
166
- min_match, lazy)
166
+ min_match, lazy, ldm = nil,
167
+ max_distance = BLOCK_MAX_SIZE)
167
168
  mm = [min_match, MIN_MATCH_ECONOMICAL].max
168
169
  span = block_end - block_start
169
170
  if span < mm + 1
@@ -177,7 +178,16 @@ module Omnizip
177
178
  limit = block_end - mm
178
179
 
179
180
  while ip < limit
180
- match = find_best_match(src, ip, ms, mm, limit)
181
+ match = if ldm
182
+ find_match_ldm(src, ip, ms, mm, limit, anchor,
183
+ seq_store.rep_offsets[0], ldm,
184
+ max_distance)
185
+ elsif lazy.zero?
186
+ find_greedy_match(src, ip, ms, mm, limit, anchor,
187
+ seq_store.rep_offsets[0])
188
+ else
189
+ find_best_match(src, ip, ms, mm, limit)
190
+ end
181
191
  if match
182
192
  dist, len = match
183
193
  defer = false
@@ -185,7 +195,12 @@ module Omnizip
185
195
  (1..lazy).each do |k|
186
196
  next unless ip + k < limit
187
197
 
188
- m2 = probe_match(src, ip + k, ms, mm, limit)
198
+ m2 = if ldm
199
+ probe_match(src, ip + k, ms, mm, limit, ldm,
200
+ max_distance)
201
+ else
202
+ probe_match(src, ip + k, ms, mm, limit)
203
+ end
189
204
  if m2 && m2[1] > len + k
190
205
  defer = true
191
206
  break
@@ -198,6 +213,7 @@ module Omnizip
198
213
  next
199
214
  end
200
215
 
216
+ ip = match[2] if match.length == 3
201
217
  seq_store.literals << src.byteslice(anchor, ip - anchor)
202
218
  seq_store.sequences <<
203
219
  RawSequence.new(ip - anchor, len, dist)
@@ -218,6 +234,71 @@ module Omnizip
218
234
  # rubocop:enable Metrics/AbcSize
219
235
  # rubocop:enable Metrics/MethodLength
220
236
 
237
+ # LDM-mode match search (port of the Rust
238
+ # compress_block_lazy2_with_ldm loop body): rep0 fast-path
239
+ # first, then the normal hash probe merged with the LDM
240
+ # table. Returns [offset, length, start] or nil.
241
+ def find_match_ldm(src, ip, ms, min_match, limit, anchor, rep0,
242
+ ldm, max_distance)
243
+ m = rep0_match(src, ip, rep0, min_match, limit, anchor)
244
+ return m if m
245
+
246
+ dist, len = find_best_match(src, ip, ms, min_match, limit,
247
+ ldm, max_distance)
248
+ return nil unless dist
249
+
250
+ [dist, len, ip]
251
+ end
252
+
253
+ # Greedy-path match search (port of the Rust
254
+ # compress_block_with_min_match): the rep0 fast-path first
255
+ # (cheapest offset to encode), then the hash table — both
256
+ # with backward extension into the pending literals. Returns
257
+ # [offset, length, start] or nil.
258
+ def find_greedy_match(src, ip, ms, min_match, limit, anchor, rep0)
259
+ m = rep0_match(src, ip, rep0, min_match, limit, anchor)
260
+ return m if m
261
+
262
+ dist, len = find_best_match(src, ip, ms, min_match, limit)
263
+ return nil unless dist
264
+
265
+ back = backward_extension(src, ip, anchor, ip - dist)
266
+ [dist, len + back, ip - back]
267
+ end
268
+
269
+ # Match at distance rep0 (the decoder's most recent offset)
270
+ # with forward and backward extension. [rep0, length, start]
271
+ # or nil.
272
+ #
273
+ # rubocop:disable-next Metrics/AbcSize
274
+ def rep0_match(src, ip, rep0, min_match, limit, anchor)
275
+ return nil if rep0 <= 0 || ip <= rep0
276
+ return nil if src.byteslice(ip, MIN_MATCH) !=
277
+ src.byteslice(ip - rep0, MIN_MATCH)
278
+
279
+ m_len = MIN_MATCH + count_match(
280
+ src, ip + MIN_MATCH, src, ip + MIN_MATCH - rep0,
281
+ [limit + MIN_MATCH_ECONOMICAL - ip - MIN_MATCH, 0].max
282
+ )
283
+ back = backward_extension(src, ip, anchor, ip - rep0)
284
+ m_len += back
285
+ return nil if m_len < min_match
286
+
287
+ [rep0, m_len, ip - back]
288
+ end
289
+
290
+ # Bytes before `ip` (and before the match candidate) that
291
+ # extend the match backward, bounded by the pending literal
292
+ # run so the previous sequence is never overlapped.
293
+ def backward_extension(src, ip, anchor, candidate)
294
+ back = 0
295
+ while ip > anchor + back && candidate > back &&
296
+ src.getbyte(ip - 1 - back) == src.getbyte(candidate - 1 - back)
297
+ back += 1
298
+ end
299
+ back
300
+ end
301
+
221
302
  # Find the best match at `ip` (single probe, or a chain walk
222
303
  # when enabled). Updates the hash table. Returns
223
304
  # [distance, length] or nil.
@@ -225,7 +306,8 @@ module Omnizip
225
306
  # rubocop:disable Metrics/MethodLength
226
307
  # rubocop:disable Metrics/AbcSize
227
308
  # rubocop:disable Metrics/CyclomaticComplexity
228
- def find_best_match(src, ip, ms, min_match, limit)
309
+ def find_best_match(src, ip, ms, min_match, limit, ldm = nil,
310
+ max_distance = BLOCK_MAX_SIZE)
229
311
  size = src.bytesize
230
312
  return nil if ip + MIN_MATCH > size
231
313
 
@@ -245,7 +327,7 @@ module Omnizip
245
327
  break if candidate.zero? || candidate >= ip
246
328
 
247
329
  dist = ip - candidate
248
- break if dist >= BLOCK_MAX_SIZE
330
+ break if dist >= max_distance
249
331
  break if candidate + MIN_MATCH > size
250
332
 
251
333
  if src.byteslice(ip, MIN_MATCH) == src.byteslice(candidate, MIN_MATCH)
@@ -264,6 +346,15 @@ module Omnizip
264
346
  candidate = ms.chain[candidate]
265
347
  end
266
348
 
349
+ if ldm
350
+ lm = ldm.find_match(src, ip, max_distance, min_match,
351
+ limit + min_match)
352
+ if lm && lm[1] > best_len
353
+ best_len = lm[1]
354
+ best_dist = lm[0]
355
+ end
356
+ end
357
+
267
358
  return nil if best_len < min_match
268
359
 
269
360
  [best_dist, best_len]
@@ -278,23 +369,39 @@ module Omnizip
278
369
  #
279
370
  # rubocop:disable Metrics/AbcSize
280
371
  # rubocop:disable Metrics/CyclomaticComplexity
281
- def probe_match(src, ip, ms, min_match, limit)
372
+ def probe_match(src, ip, ms, min_match, limit, ldm = nil,
373
+ max_distance = BLOCK_MAX_SIZE)
282
374
  size = src.bytesize
283
375
  return nil if ip + MIN_MATCH > size
284
376
 
285
377
  candidate = ms.hash_table[hash4(src, ip, ms.hash_log)]
286
- return nil if candidate.zero? || candidate >= ip
287
378
 
288
- dist = ip - candidate
289
- return nil if dist >= BLOCK_MAX_SIZE
290
- return nil if candidate + MIN_MATCH > size
291
- return nil if src.byteslice(ip, MIN_MATCH) !=
292
- src.byteslice(candidate, MIN_MATCH)
379
+ best_len = 0
380
+ best_dist = 0
381
+ if candidate.positive? && candidate < ip
382
+ dist = ip - candidate
383
+ if dist < max_distance && candidate + MIN_MATCH <= size &&
384
+ src.byteslice(ip, MIN_MATCH) == src.byteslice(candidate, MIN_MATCH)
385
+ best_len = MIN_MATCH + count_match(
386
+ src, ip + MIN_MATCH, src, candidate + MIN_MATCH,
387
+ [limit + MIN_MATCH_ECONOMICAL - ip - MIN_MATCH, 0].max
388
+ )
389
+ best_dist = dist
390
+ end
391
+ end
392
+
393
+ if ldm
394
+ lm = ldm.find_match(src, ip, max_distance, min_match,
395
+ limit + min_match)
396
+ if lm && lm[1] > best_len
397
+ best_len = lm[1]
398
+ best_dist = lm[0]
399
+ end
400
+ end
401
+
402
+ return nil if best_len < min_match
293
403
 
294
- m_len = MIN_MATCH + count_match(src, ip + MIN_MATCH,
295
- src, candidate + MIN_MATCH,
296
- [limit + MIN_MATCH_ECONOMICAL - ip - MIN_MATCH, 0].max)
297
- m_len >= min_match ? [dist, m_len] : nil
404
+ [best_dist, best_len]
298
405
  end
299
406
  # rubocop:enable Metrics/AbcSize
300
407
  # rubocop:enable Metrics/CyclomaticComplexity
@@ -57,6 +57,7 @@ module Omnizip
57
57
  autoload :SequencesEncoder,
58
58
  "omnizip/algorithms/zstandard/sequences_encoder"
59
59
  autoload :MatchFinder, "omnizip/algorithms/zstandard/match_finder"
60
+ autoload :LdmHashTable, "omnizip/algorithms/zstandard/ldm"
60
61
  autoload :XXHash64, "omnizip/algorithms/zstandard/xxhash"
61
62
 
62
63
  # Frame and FSE modules
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.16"
4
+ VERSION = "0.3.18"
5
5
  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.16
4
+ version: 0.3.18
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-08-24 00:00:00.000000000 Z
11
+ date: 2026-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -315,6 +315,7 @@ files:
315
315
  - lib/omnizip/algorithms/zstandard/fse/table_description.rb
316
316
  - lib/omnizip/algorithms/zstandard/huffman.rb
317
317
  - lib/omnizip/algorithms/zstandard/huffman_encoder.rb
318
+ - lib/omnizip/algorithms/zstandard/ldm.rb
318
319
  - lib/omnizip/algorithms/zstandard/literals.rb
319
320
  - lib/omnizip/algorithms/zstandard/literals_encoder.rb
320
321
  - lib/omnizip/algorithms/zstandard/match_finder.rb