omnizip 0.3.17 → 0.3.19

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: affda0906d52a418254670adb02e96312d6e268d1df4aafbba575ade503edfbe
4
+ data.tar.gz: ae4e84af21221350bf4d67a1b295546674527e091b500a715cb96f8547870990
5
5
  SHA512:
6
- metadata.gz: ed59928f26cec56627b12304d9cf55d3ed04a7ee07f0a3bf2ef6f71b44a0e21d76d4246746db8f5dd0751139f68208a5a5a53c81ce9959f4433f883d3648adfc
7
- data.tar.gz: 311d21ef3715257f1db58f9c1238200d04dce604022337fa53adf7a2c95d1a2d1272ba75bbcae55aaaf5d5b97a16ef954e7ab1b423e9f6875abb1f1036653a6b
6
+ metadata.gz: 91044c5b1067662c5177e2b8ca262ad0434e4a4cf94e9ea321251723499188fc4b57b3793e2ed88ce51ea406cdb1bfb6d31921ef3b08a7baaf4fd6e62102dc5b
7
+ data.tar.gz: b6614848a2eb450b748bca1d3d297b7bcd5751607124687237c75ebdeeb43b033ffe6d9af49af457e94aa1cace5ab615c3fe6d04f6a5bb3206a863249225ead6
@@ -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
@@ -20,506 +20,35 @@
20
20
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
21
  # DEALINGS IN THE SOFTWARE.
22
22
 
23
- require "stringio"
24
-
25
23
  module Omnizip
26
24
  module Implementations
27
25
  module SevenZip
28
26
  module LZMA2
29
- # 7-Zip SDK LZMA2 encoder implementation.
30
- #
31
- # This encoder produces LZMA2 compressed data compatible with 7-Zip format.
32
- # It uses the same LZMA encoding logic as XZ Utils, but with 7-Zip
33
- # format requirements (no EOS marker, no padding).
27
+ # 7-Zip LZMA2 encoder.
34
28
  #
35
- # Key differences from XZ Utils implementation:
36
- # - No EOS marker (raw LZMA2 data ends with 0x00 control byte)
37
- # - No chunk padding (XZ pads to 4-byte boundary)
38
- # - No LZMA2 property byte in data stream (method ID only in container)
29
+ # The LZMA2 stream itself is format-identical to the raw LZMA2
30
+ # layer of the XZ Utils encoder (chunks + 0x00 end marker); the
31
+ # 7-Zip container carries the method properties, so no property
32
+ # byte is written into the data stream (standalone: false).
39
33
  #
40
- # Based on LZMA SDK by Igor Pavlov
41
- # Reference: https://www.7-zip.org/sdk.html
42
- #
43
- # LZMA2 format (as used by 7-Zip):
44
- # - Control byte specifies chunk type and dictionary reset
45
- # - Dictionary size follows in some chunk types
46
- # - Uncompressed size follows in some chunk types
47
- # - Compressed data follows
48
- class Encoder < Base::LZMA2EncoderBase
49
- include Omnizip::Algorithms::LZMA2Const
50
-
51
- # Maximum chunk sizes (from LZMA2 specification)
52
- MAX_UNCOMPRESSED_CHUNK = 2 * 1024 * 1024 # 2MB
53
- MAX_COMPRESSED_CHUNK = 64 * 1024 # 64KB
54
-
55
- # Encoding constants
56
- UINT32_MAX = 0xFFFFFFFF
57
- REPS = 4
58
- MATCH_LEN_MIN = 2
59
-
60
- attr_reader :dict_size, :lc, :lp, :pb, :standalone
61
-
62
- # Initialize 7-Zip SDK LZMA2 encoder
63
- #
64
- # @param dict_size [Integer] Dictionary size (must be power of 2)
65
- # @param lc [Integer] Literal context bits (0-8)
66
- # @param lp [Integer] Literal position bits (0-4)
67
- # @param pb [Integer] Position bits (0-4)
68
- # @param standalone [Boolean] Include property byte (false for 7-Zip)
69
- def initialize(dict_size:, lc: 3, lp: 0, pb: 2, standalone: false)
70
- super
71
-
72
- # Initialize shared state across all chunks
73
- # Using XZ Utils components (tested and working)
74
-
75
- @dictionary = Omnizip::Algorithms::LZMA::Dictionary.new(dict_size)
76
- @state = Omnizip::Algorithms::LZMA::LZMAState.new(0)
77
- @models = Omnizip::Algorithms::LZMA::XzProbabilityModels.new(lc,
78
- lp, pb)
79
- @match_finder = Omnizip::Algorithms::LZMA::MatchFinder.new(@dictionary)
80
- @optimal = Omnizip::Algorithms::LZMA::OptimalEncoder.new(mode: :fast)
81
-
82
- # Track previous byte for literal context
83
- @prev_byte = 0
84
-
85
- # First chunk always resets dictionary (7-Zip compatibility)
86
- @need_dictionary_reset = true
87
- @need_state_reset = false
88
- @need_properties = true
89
- end
90
-
91
- # Encode data with LZMA2 compression
92
- #
93
- # @param data [String] Input data to compress
94
- # @return [String] LZMA2 compressed data (7-Zip format)
95
- def encode(data)
96
- return "" if data.empty?
97
-
98
- output = StringIO.new
99
- output.set_encoding(Encoding::BINARY)
100
-
101
- # Write property byte if standalone mode
102
- if @standalone
103
- prop_byte = encode_dict_size(@dict_size)
104
- output.putc(prop_byte)
105
- end
106
-
107
- # Reset match finder state for each encoding session
108
- @match_finder.reset
109
-
110
- # Process in chunks
111
- input = StringIO.new(data)
112
- input.set_encoding(Encoding::BINARY)
113
-
114
- while !input.eof?
115
- chunk_data = input.read(MAX_UNCOMPRESSED_CHUNK)
116
- break if chunk_data.nil? || chunk_data.empty?
117
-
118
- chunk = encode_chunk(chunk_data)
119
- output.write(chunk)
120
-
121
- # Update reset flags for next chunk
122
- @need_dictionary_reset = false
123
- @need_state_reset = false
124
- @need_properties = false
125
- end
126
-
127
- # End of stream marker (0x00)
128
- output.write(Omnizip::Algorithms::LZMA2::LZMA2Chunk.end_chunk.to_bytes)
129
-
130
- output.string
131
- end
132
-
133
- # Get implementation identifier
34
+ # This is a thin subclass rather than a copy: the XZ Utils
35
+ # encoder carries the fixes for global pos_state across
36
+ # chunks, announced state resets, the 64 KiB compressed-chunk
37
+ # cap (16-bit size field) and the range-encoder symbol-queue
38
+ # drain, which a duplicated implementation silently missed.
39
+ class Encoder < Implementations::XZUtils::LZMA2::Encoder
40
+ # 7-Zip callers pass the same option keys; only the property
41
+ # byte default differs (the container describes the coder).
42
+ def initialize(options = {})
43
+ super({ standalone: false }.merge(options))
44
+ end
45
+
46
+ # Get implementation identifier.
134
47
  #
135
48
  # @return [Symbol] :seven_zip_sdk
136
49
  def implementation_name
137
50
  :seven_zip_sdk
138
51
  end
139
-
140
- private
141
-
142
- # Encode a single chunk with LZMA2 compression
143
- #
144
- # Uses XZ Utils encoding logic (tested and compatible)
145
- def encode_chunk(uncompressed_data)
146
- compressed = try_compress(uncompressed_data)
147
-
148
- # Decide: compressed vs uncompressed
149
- # Use compressed if it's actually smaller
150
- if compressed.bytesize >= uncompressed_data.bytesize
151
- # Use uncompressed chunk
152
- chunk = Omnizip::Algorithms::LZMA2::LZMA2Chunk.new(
153
- chunk_type: :uncompressed,
154
- uncompressed_data: uncompressed_data,
155
- compressed_data: "",
156
- need_dict_reset: @need_dictionary_reset,
157
- need_state_reset: false,
158
- need_props: false,
159
- )
160
- # After uncompressed chunk, next chunk needs state reset
161
- @need_state_reset = true
162
- else
163
- # Use compressed chunk
164
- chunk_properties = (((@pb * 5) + @lp) * 9) + @lc
165
- chunk = Omnizip::Algorithms::LZMA2::LZMA2Chunk.new(
166
- chunk_type: :compressed,
167
- uncompressed_data: uncompressed_data,
168
- compressed_data: compressed,
169
- compressed_size: compressed.bytesize,
170
- properties: chunk_properties,
171
- need_dict_reset: @need_dictionary_reset,
172
- need_state_reset: @need_state_reset,
173
- need_props: true,
174
- )
175
- end
176
-
177
- # Update dictionary with the chunk data
178
- @dictionary.append(uncompressed_data)
179
-
180
- # Update prev_byte for next chunk
181
- if uncompressed_data.bytesize.positive?
182
- @prev_byte = uncompressed_data.getbyte(uncompressed_data.bytesize - 1)
183
- end
184
-
185
- chunk.to_bytes
186
- end
187
-
188
- # Try to compress data using LZMA
189
- #
190
- # Uses XZ Utils encoding components (tested and working)
191
- def try_compress(data)
192
- # Create output buffer
193
- output_buffer = StringIO.new
194
- output_buffer.set_encoding(Encoding::BINARY)
195
-
196
- # Create range encoder
197
- encoder = Omnizip::Algorithms::LZMA::XzRangeEncoder.new(output_buffer)
198
-
199
- # Feed all data to match finder first
200
- @match_finder.feed(data)
201
-
202
- # Initialize hash table
203
- match_len_max = 2
204
- end_pos = [
205
- @dictionary.buffer.bytesize + data.bytesize - match_len_max, 0
206
- ].max
207
- @match_finder.skip(end_pos)
208
-
209
- # Position in match finder's buffer for encoding
210
- start_pos = @dictionary.buffer.bytesize
211
- @current_start_pos = start_pos
212
-
213
- pos = 0
214
- while pos < data.bytesize
215
- # Encode queued symbols if buffer getting full
216
- if encoder.count > 20
217
- encode_queued_symbols(encoder, output_buffer)
218
- end
219
-
220
- # Find matches at current position
221
- match_pos = start_pos + pos
222
- @match_finder.find_matches(match_pos)
223
-
224
- # Get optimal encoding choice
225
- distance, length = @optimal.find_optimal(
226
- match_pos,
227
- @match_finder,
228
- @state,
229
- @state.reps,
230
- @models,
231
- )
232
-
233
- # Encode based on choice
234
- if distance == UINT32_MAX || length == 1
235
- encode_literal(data.getbyte(pos), encoder, pos)
236
- pos += 1
237
- elsif distance < REPS
238
- encode_repeated_match(distance, length, encoder, pos, match_pos)
239
- pos += length
240
- else
241
- actual_distance = distance - REPS
242
- encode_match(actual_distance, length, encoder, pos, match_pos,
243
- data)
244
- pos += length
245
- end
246
- end
247
-
248
- # Flush encoder
249
- encode_queued_symbols(encoder, output_buffer)
250
- encoder.queue_flush
251
- encode_queued_symbols(encoder, output_buffer)
252
-
253
- output_buffer.string
254
- end
255
-
256
- # Encode queued symbols to output
257
- def encode_queued_symbols(encoder, output)
258
- return if encoder.none?
259
-
260
- temp_buffer = "\0" * 10000
261
- out_pos = Omnizip::Algorithms::LZMA::IntRef.new(0)
262
-
263
- size_before = output.size
264
-
265
- encoder.encode_symbols(temp_buffer, out_pos, 10000)
266
-
267
- if out_pos.value.positive?
268
- output.write(StringCompat.byteslice(temp_buffer, 0,
269
- out_pos.value))
270
- end
271
-
272
- output.size - size_before
273
- end
274
-
275
- # Compatibility helper for Ruby 3.0-3.1
276
- module StringCompat
277
- if "".respond_to?(:byteslice)
278
- def self.byteslice(string, start, length)
279
- string.byteslice(start, length)
280
- end
281
- else
282
- def self.byteslice(string, start, length)
283
- string.bytes[start, length]&.pack("C*") || ""
284
- end
285
- end
286
- end
287
-
288
- # Encode literal byte
289
- def encode_literal(symbol, encoder, pos)
290
- pos_state = pos & ((1 << @pb) - 1)
291
-
292
- prob_is_match = @models.is_match[@state.value][pos_state]
293
- encoder.queue_bit(prob_is_match, 0)
294
-
295
- literal_offset = get_literal_state(pos, @prev_byte)
296
- use_matched = @state.use_matched_literal?
297
-
298
- @state.update_literal!
299
-
300
- if use_matched
301
- match_pos = @current_start_pos + pos
302
- match_byte_pos = match_pos - @state.reps[0] - 1
303
- match_byte = @match_finder.buffer.getbyte(match_byte_pos) if match_byte_pos >= 0 && match_byte_pos < @match_finder.buffer.bytesize
304
-
305
- if match_byte.nil?
306
- encode_normal_literal(literal_offset, symbol, encoder)
307
- else
308
- encode_matched_literal(literal_offset, match_byte, symbol,
309
- encoder)
310
- end
311
- else
312
- encode_normal_literal(literal_offset, symbol, encoder)
313
- end
314
-
315
- @prev_byte = symbol
316
- end
317
-
318
- # Encode normal match
319
- def encode_match(distance, length, encoder, pos, match_pos,
320
- _input_data)
321
- pos_state = pos & ((1 << @pb) - 1)
322
-
323
- prob_is_match = @models.is_match[@state.value][pos_state]
324
- encoder.queue_bit(prob_is_match, 1)
325
-
326
- prob_is_rep = @models.is_rep[@state.value]
327
- encoder.queue_bit(prob_is_rep, 0)
328
-
329
- # Reps are stored 0-based (distance - 1), matching the decoder's
330
- # rep0 convention.
331
- @state.update_match!(distance - 1)
332
-
333
- encode_match_length(length, pos_state, encoder)
334
- encode_distance(distance, length, encoder)
335
-
336
- last_byte_pos = match_pos - distance + length - 1
337
- @prev_byte = @match_finder.buffer.getbyte(last_byte_pos) if last_byte_pos >= 0 && last_byte_pos < @match_finder.buffer.bytesize
338
- end
339
-
340
- # Encode repeated match
341
- def encode_repeated_match(rep, length, encoder, pos, match_pos)
342
- pos_state = pos & ((1 << @pb) - 1)
343
-
344
- prob_is_match = @models.is_match[@state.value][pos_state]
345
- encoder.queue_bit(prob_is_match, 1)
346
-
347
- prob_is_rep = @models.is_rep[@state.value]
348
- encoder.queue_bit(prob_is_rep, 1)
349
-
350
- prob_is_rep0 = @models.is_rep0[@state.value]
351
- if rep.zero?
352
- encoder.queue_bit(prob_is_rep0, 0)
353
-
354
- prob_is_rep0_long = @models.is_rep0_long[@state.value][pos_state]
355
- encoder.queue_bit(prob_is_rep0_long, length == 1 ? 0 : 1)
356
- else
357
- encoder.queue_bit(prob_is_rep0, 1)
358
-
359
- prob_is_rep1 = @models.is_rep1[@state.value]
360
- if rep == 1
361
- encoder.queue_bit(prob_is_rep1, 0)
362
- else
363
- encoder.queue_bit(prob_is_rep1, 1)
364
-
365
- prob_is_rep2 = @models.is_rep2[@state.value]
366
- encoder.queue_bit(prob_is_rep2, rep - 2)
367
- end
368
-
369
- # XZ Utils rep_match: capture the selected distance BEFORE
370
- # rotating, then shift reps down.
371
- distance = @state.reps[rep]
372
- rep.downto(1) { |i| @state.reps[i] = @state.reps[i - 1] }
373
- @state.reps[0] = distance
374
- end
375
-
376
- if length == 1
377
- @state.update_short_rep!
378
- else
379
- # Rep matches use their own length models (decoder's
380
- # rep_length_coder).
381
- encode_match_length(length, pos_state, encoder,
382
- @models.rep_len_encoder)
383
- @state.update_long_rep!
384
- end
385
-
386
- # Last byte of the match region in the stream
387
- last_byte_pos = match_pos + length - 1
388
- @prev_byte = @match_finder.buffer.getbyte(last_byte_pos) if last_byte_pos >= 0 && last_byte_pos < @match_finder.buffer.bytesize
389
- end
390
-
391
- def get_literal_state(pos, prev_byte)
392
- literal_mask = (0x100 << @lp) - (0x100 >> @lc)
393
- # Factor of 3: each literal context owns a 0x300-sized subcoder
394
- # (XZ Utils literal_subcoder macro); must match the decoder's
395
- # base_offset = 3 * (lit_state << lc).
396
- 3 * ((((pos << 8) + prev_byte) & literal_mask) << @lc)
397
- end
398
-
399
- def encode_normal_literal(literal_offset, symbol, encoder)
400
- context = 1
401
- 8.downto(1) do |i|
402
- bit = (symbol >> (i - 1)) & 1
403
- encoder.queue_bit(@models.literal[literal_offset + context], bit)
404
- context = (context << 1) | bit
405
- end
406
- end
407
-
408
- def encode_matched_literal(literal_offset, match_byte, symbol,
409
- encoder)
410
- offset = 0x100
411
- symbol += 0x100
412
-
413
- while symbol < 0x10000
414
- match_byte <<= 1
415
- match_bit = match_byte & offset
416
- subcoder_index = offset + match_bit + (symbol >> 8)
417
- bit = (symbol >> 7) & 1
418
-
419
- encoder.queue_bit(
420
- @models.literal[literal_offset + subcoder_index], bit
421
- )
422
-
423
- symbol <<= 1
424
- offset &= ~(match_byte ^ symbol)
425
- end
426
- end
427
-
428
- # len_encoder selects the length model set: match_len_encoder for
429
- # normal matches, rep_len_encoder for repeated matches.
430
- def encode_match_length(length, pos_state, encoder,
431
- len_encoder = @models.match_len_encoder)
432
- len = length - 2
433
-
434
- if len < 8
435
- encoder.queue_bit(len_encoder.choice, 0)
436
- encode_bittree(len_encoder.low[pos_state], 3, len, encoder)
437
- elsif len < 16
438
- encoder.queue_bit(len_encoder.choice, 1)
439
- encoder.queue_bit(len_encoder.choice2, 0)
440
- encode_bittree(len_encoder.mid[pos_state], 3, len - 8, encoder)
441
- else
442
- encoder.queue_bit(len_encoder.choice, 1)
443
- encoder.queue_bit(len_encoder.choice2, 1)
444
- encode_bittree(len_encoder.high, 8, len - 16, encoder)
445
- end
446
- end
447
-
448
- def encode_distance(distance, length, encoder)
449
- # get_dist_slot expects a 0-based distance (xz fastpos.h:
450
- # dist 4 -> slot 4, dist 5 -> slot 4).
451
- dist0 = distance - 1
452
- dist_slot = get_dist_slot(dist0)
453
- len_state = [length - 2, 3].min
454
-
455
- encode_bittree(@models.dist_slot[len_state], 6, dist_slot, encoder)
456
-
457
- if dist_slot >= 4
458
- footer_bits = (dist_slot >> 1) - 1
459
- base = (2 | (dist_slot & 1)) << footer_bits
460
- dist_reduced = dist0 - base
461
-
462
- if dist_slot < 14
463
- encode_bittree_reverse(@models.dist_special, dist_reduced,
464
- footer_bits, base - dist_slot - 1, encoder)
465
- else
466
- direct_bits = footer_bits - 4
467
- encoder.queue_direct_bits(dist_reduced >> 4, direct_bits)
468
- align_mask = (1 << 4) - 1
469
- encode_bittree_reverse(@models.dist_align,
470
- dist_reduced & align_mask, 4, 0, encoder)
471
- end
472
- end
473
- end
474
-
475
- def encode_bittree(probs, num_bits, value, encoder)
476
- context = 1
477
- num_bits.downto(1) do |i|
478
- bit = (value >> (i - 1)) & 1
479
- encoder.queue_bit(probs[context], bit)
480
- context = (context << 1) | bit
481
- end
482
- end
483
-
484
- def encode_bittree_reverse(probs, value, num_bits, offset, encoder)
485
- context = 1
486
- num_bits.times do |i|
487
- bit = (value >> i) & 1
488
- encoder.queue_bit(probs[offset + context], bit)
489
- context = (context << 1) | bit
490
- end
491
- end
492
-
493
- def get_dist_slot(distance)
494
- if distance < 4
495
- distance
496
- else
497
- slot = 0
498
- dist = distance
499
- while dist > 3
500
- dist >>= 1
501
- slot += 2
502
- end
503
- slot + dist
504
- end
505
- end
506
-
507
- def encode_dict_size(dict_size)
508
- d = [dict_size, DICT_SIZE_MIN].max
509
-
510
- log2_size = 0
511
- temp = d
512
- while temp > 1
513
- log2_size += 1
514
- temp >>= 1
515
- end
516
-
517
- if d == (1 << log2_size)
518
- [(log2_size - 12) * 2, 40].min
519
- else
520
- [((log2_size - 12) * 2) + 1, 40].min
521
- end
522
- end
523
52
  end
524
53
  end
525
54
  end
@@ -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.19"
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.19
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