omnizip 0.3.17 → 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: 26bb587688afe9104a60ed3db30bd0b4d4a0ad909d1f02d0afaf3c8b67343119
4
- data.tar.gz: 674f6bdf6fa36ec488ad06aa3f0a98ba792945fff3d244e9913ee06ff64402a6
3
+ metadata.gz: 689a5e44cef4327034541bbcdca28c15e75da2d6cb2452a0617365e107bf1597
4
+ data.tar.gz: d9bd5f70930e7efdb4fcaaba779927c0b0898e163e79e8e059a990ccc4083c31
5
5
  SHA512:
6
- metadata.gz: ed59928f26cec56627b12304d9cf55d3ed04a7ee07f0a3bf2ef6f71b44a0e21d76d4246746db8f5dd0751139f68208a5a5a53c81ce9959f4433f883d3648adfc
7
- data.tar.gz: 311d21ef3715257f1db58f9c1238200d04dce604022337fa53adf7a2c95d1a2d1272ba75bbcae55aaaf5d5b97a16ef954e7ab1b423e9f6875abb1f1036653a6b
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,11 @@ module Omnizip
177
178
  limit = block_end - mm
178
179
 
179
180
  while ip < limit
180
- match = if lazy.zero?
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?
181
186
  find_greedy_match(src, ip, ms, mm, limit, anchor,
182
187
  seq_store.rep_offsets[0])
183
188
  else
@@ -190,7 +195,12 @@ module Omnizip
190
195
  (1..lazy).each do |k|
191
196
  next unless ip + k < limit
192
197
 
193
- 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
194
204
  if m2 && m2[1] > len + k
195
205
  defer = true
196
206
  break
@@ -224,6 +234,22 @@ module Omnizip
224
234
  # rubocop:enable Metrics/AbcSize
225
235
  # rubocop:enable Metrics/MethodLength
226
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
+
227
253
  # Greedy-path match search (port of the Rust
228
254
  # compress_block_with_min_match): the rep0 fast-path first
229
255
  # (cheapest offset to encode), then the hash table — both
@@ -280,7 +306,8 @@ module Omnizip
280
306
  # rubocop:disable Metrics/MethodLength
281
307
  # rubocop:disable Metrics/AbcSize
282
308
  # rubocop:disable Metrics/CyclomaticComplexity
283
- 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)
284
311
  size = src.bytesize
285
312
  return nil if ip + MIN_MATCH > size
286
313
 
@@ -300,7 +327,7 @@ module Omnizip
300
327
  break if candidate.zero? || candidate >= ip
301
328
 
302
329
  dist = ip - candidate
303
- break if dist >= BLOCK_MAX_SIZE
330
+ break if dist >= max_distance
304
331
  break if candidate + MIN_MATCH > size
305
332
 
306
333
  if src.byteslice(ip, MIN_MATCH) == src.byteslice(candidate, MIN_MATCH)
@@ -319,6 +346,15 @@ module Omnizip
319
346
  candidate = ms.chain[candidate]
320
347
  end
321
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
+
322
358
  return nil if best_len < min_match
323
359
 
324
360
  [best_dist, best_len]
@@ -333,23 +369,39 @@ module Omnizip
333
369
  #
334
370
  # rubocop:disable Metrics/AbcSize
335
371
  # rubocop:disable Metrics/CyclomaticComplexity
336
- 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)
337
374
  size = src.bytesize
338
375
  return nil if ip + MIN_MATCH > size
339
376
 
340
377
  candidate = ms.hash_table[hash4(src, ip, ms.hash_log)]
341
- return nil if candidate.zero? || candidate >= ip
342
378
 
343
- dist = ip - candidate
344
- return nil if dist >= BLOCK_MAX_SIZE
345
- return nil if candidate + MIN_MATCH > size
346
- return nil if src.byteslice(ip, MIN_MATCH) !=
347
- 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
348
392
 
349
- m_len = MIN_MATCH + count_match(src, ip + MIN_MATCH,
350
- src, candidate + MIN_MATCH,
351
- [limit + MIN_MATCH_ECONOMICAL - ip - MIN_MATCH, 0].max)
352
- m_len >= min_match ? [dist, m_len] : nil
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
403
+
404
+ [best_dist, best_len]
353
405
  end
354
406
  # rubocop:enable Metrics/AbcSize
355
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.17"
4
+ VERSION = "0.3.18"
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.17
4
+ version: 0.3.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -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