omnizip 0.3.13 → 0.3.15

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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/lib/omnizip/algorithm.rb +36 -1
  3. data/lib/omnizip/algorithms/bzip2.rb +1 -3
  4. data/lib/omnizip/algorithms/deflate.rb +1 -3
  5. data/lib/omnizip/algorithms/deflate64.rb +2 -2
  6. data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +7 -8
  7. data/lib/omnizip/algorithms/lzma.rb +1 -1
  8. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +5 -1
  9. data/lib/omnizip/algorithms/lzma2.rb +4 -12
  10. data/lib/omnizip/algorithms/zstandard/constants.rb +38 -25
  11. data/lib/omnizip/algorithms/zstandard/decoder.rb +101 -136
  12. data/lib/omnizip/algorithms/zstandard/encoder.rb +81 -138
  13. data/lib/omnizip/algorithms/zstandard/frame/block.rb +17 -0
  14. data/lib/omnizip/algorithms/zstandard/frame/header.rb +42 -29
  15. data/lib/omnizip/algorithms/zstandard/fse/bitstream.rb +114 -117
  16. data/lib/omnizip/algorithms/zstandard/fse/encoder.rb +434 -222
  17. data/lib/omnizip/algorithms/zstandard/fse/interleaved.rb +92 -0
  18. data/lib/omnizip/algorithms/zstandard/fse/table.rb +98 -176
  19. data/lib/omnizip/algorithms/zstandard/fse/table_description.rb +203 -0
  20. data/lib/omnizip/algorithms/zstandard/fse.rb +21 -2
  21. data/lib/omnizip/algorithms/zstandard/huffman.rb +195 -188
  22. data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +232 -255
  23. data/lib/omnizip/algorithms/zstandard/literals.rb +170 -103
  24. data/lib/omnizip/algorithms/zstandard/literals_encoder.rb +47 -199
  25. data/lib/omnizip/algorithms/zstandard/sequences.rb +260 -252
  26. data/lib/omnizip/algorithms/zstandard/xxhash.rb +159 -0
  27. data/lib/omnizip/algorithms/zstandard.rb +20 -24
  28. data/lib/omnizip/algorithms.rb +14 -1
  29. data/lib/omnizip/buffer.rb +1 -2
  30. data/lib/omnizip/commands/archive_list_command.rb +9 -7
  31. data/lib/omnizip/convenience.rb +1 -2
  32. data/lib/omnizip/entry.rb +44 -0
  33. data/lib/omnizip/extraction/selective_extractor.rb +2 -10
  34. data/lib/omnizip/filter_pipeline.rb +1 -1
  35. data/lib/omnizip/filter_registry.rb +18 -16
  36. data/lib/omnizip/filters/bcj2.rb +1 -1
  37. data/lib/omnizip/filters/bcj_arm.rb +1 -1
  38. data/lib/omnizip/filters/bcj_arm64.rb +1 -1
  39. data/lib/omnizip/filters/bcj_ia64.rb +1 -1
  40. data/lib/omnizip/filters/bcj_ppc.rb +1 -1
  41. data/lib/omnizip/filters/bcj_sparc.rb +1 -1
  42. data/lib/omnizip/filters/bcj_x86.rb +1 -1
  43. data/lib/omnizip/filters.rb +0 -2
  44. data/lib/omnizip/formats/bzip2_file.rb +0 -7
  45. data/lib/omnizip/formats/cpio/entry.rb +6 -0
  46. data/lib/omnizip/formats/cpio.rb +0 -6
  47. data/lib/omnizip/formats/gzip.rb +0 -7
  48. data/lib/omnizip/formats/iso/directory_record.rb +7 -0
  49. data/lib/omnizip/formats/iso.rb +0 -5
  50. data/lib/omnizip/formats/lzip.rb +0 -7
  51. data/lib/omnizip/formats/lzma_alone.rb +0 -6
  52. data/lib/omnizip/formats/msi/entry.rb +6 -0
  53. data/lib/omnizip/formats/msi.rb +0 -9
  54. data/lib/omnizip/formats/ole/dirent.rb +6 -0
  55. data/lib/omnizip/formats/ole.rb +0 -9
  56. data/lib/omnizip/formats/rar.rb +0 -6
  57. data/lib/omnizip/formats/rar3/reader.rb +7 -0
  58. data/lib/omnizip/formats/rar5/reader.rb +7 -0
  59. data/lib/omnizip/formats/rpm/entry.rb +6 -0
  60. data/lib/omnizip/formats/seven_zip/models/file_entry.rb +7 -0
  61. data/lib/omnizip/formats/seven_zip.rb +0 -6
  62. data/lib/omnizip/formats/tar/entry.rb +6 -0
  63. data/lib/omnizip/formats/tar.rb +2 -10
  64. data/lib/omnizip/formats/xar/entry.rb +6 -0
  65. data/lib/omnizip/formats/xar.rb +0 -6
  66. data/lib/omnizip/formats/zip.rb +0 -6
  67. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +33 -31
  68. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +177 -163
  69. data/lib/omnizip/implementations.rb +18 -4
  70. data/lib/omnizip/io/source.rb +3 -0
  71. data/lib/omnizip/metadata/entry_metadata.rb +7 -0
  72. data/lib/omnizip/parallel/engine.rb +48 -0
  73. data/lib/omnizip/parallel/parallel_compressor.rb +5 -16
  74. data/lib/omnizip/parallel/parallel_extractor.rb +5 -16
  75. data/lib/omnizip/parallel.rb +1 -0
  76. data/lib/omnizip/profile/profile_registry.rb +1 -2
  77. data/lib/omnizip/registry.rb +0 -1
  78. data/lib/omnizip/rubyzip_compat.rb +0 -1
  79. data/lib/omnizip/temp/temp_file.rb +1 -2
  80. data/lib/omnizip/version.rb +1 -1
  81. data/lib/omnizip/zip/entry.rb +7 -0
  82. data/lib/omnizip/zip/file.rb +4 -8
  83. data/lib/omnizip.rb +1 -1
  84. metadata +7 -6
  85. data/lib/omnizip/filters/filter_base.rb +0 -6
  86. data/lib/omnizip/filters/registration.rb +0 -22
  87. data/lib/omnizip/filters/registry.rb +0 -111
  88. data/lib/omnizip/format_registry.rb +0 -100
@@ -24,296 +24,508 @@ module Omnizip
24
24
  module Algorithms
25
25
  class Zstandard
26
26
  module FSE
27
- # FSE Encoder (RFC 8878 Section 4.1)
28
- #
29
- # Encodes symbols using Finite State Entropy coding.
30
- # FSE is a variant of arithmetic coding that uses table-based state transitions.
27
+ # FSE encoder (RFC 8878 §4.1): count normalization, NCount
28
+ # writing, CTable construction, and 2-state interleaved
29
+ # bitstream encoding. Mirrors the C reference FSE library.
31
30
  class Encoder
32
31
  include Constants
33
32
 
34
- # @return [Array<Integer>] Symbol distribution (normalized frequencies)
35
- attr_reader :distribution
33
+ RTB_TABLE = [0, 473_195, 504_333, 520_860, 550_000, 700_000,
34
+ 750_000, 830_000].freeze
35
+ NOT_YET_ASSIGNED = -2
36
36
 
37
- # @return [Integer] Accuracy log (table size = 2^accuracy_log)
38
- attr_reader :accuracy_log
37
+ SymbolTT = Struct.new(:delta_nb_bits, :delta_find_state)
39
38
 
40
- # @return [Integer] Table size
41
- attr_reader :table_size
39
+ # @return [Array<Integer>] normalized distribution
40
+ attr_reader :distribution
42
41
 
43
- # Build FSE encoder from symbol frequencies
44
- #
45
- # @param frequencies [Array<Integer>] Raw symbol frequencies
46
- # @param max_accuracy_log [Integer] Maximum accuracy log (default 9)
47
- # @return [Encoder] FSE encoder
48
- def self.build_from_frequencies(frequencies,
49
- max_accuracy_log = FSE_MAX_ACCURACY_LOG)
50
- return nil if frequencies.nil? || frequencies.empty?
51
-
52
- # Normalize frequencies to table size
53
- distribution, accuracy_log = normalize_distribution(frequencies,
54
- max_accuracy_log)
55
-
56
- new(distribution, accuracy_log)
57
- end
42
+ # @return [Integer]
43
+ attr_reader :table_log
58
44
 
59
- # Normalize frequency distribution
60
- #
61
- # Converts raw frequencies to normalized distribution that sums to 2^accuracy_log.
45
+ # @return [Integer]
46
+ attr_reader :max_symbol_value
47
+
48
+ # Normalize a raw histogram to sum to (1 << table_log).
62
49
  #
63
- # @param frequencies [Array<Integer>] Raw frequencies
64
- # @param max_accuracy_log [Integer] Maximum accuracy log
65
- # @return [Array<Array<Integer>, Integer>] Normalized distribution and accuracy log
66
- def self.normalize_distribution(frequencies, max_accuracy_log)
67
- # Count non-zero symbols
68
- total_freq = frequencies.sum
69
- return [[], 0] if total_freq.zero?
70
-
71
- # Find minimum accuracy log that fits the distribution
72
- num_symbols = frequencies.count { |f| f&.positive? }
73
- accuracy_log = [calculate_min_accuracy_log(num_symbols),
74
- FSE_MIN_ACCURACY_LOG].max
75
- accuracy_log = [accuracy_log, max_accuracy_log].min
76
-
77
- table_size = 1 << accuracy_log
78
-
79
- # Normalize frequencies to table size
80
- distribution = normalize_frequencies(frequencies, table_size)
81
-
82
- # Verify distribution sums to table size
83
- sum = distribution.sum
84
- if sum != table_size
85
- # Adjust to make it sum correctly
86
- adjust_distribution(distribution, table_size - sum)
50
+ # @param table_log [Integer]
51
+ # @param count [Array<Integer>] raw frequencies
52
+ # @param total [Integer] sum(count)
53
+ # @param max_symbol_value [Integer] highest symbol index
54
+ # @param use_low_prob [Boolean] emit -1 sentinels for rare
55
+ # symbols (the zstd default)
56
+ # @return [Array<Integer>] normalized distribution, or [] for
57
+ # an RLE stream
58
+ def self.normalize_count(table_log, count, total, max_symbol_value,
59
+ use_low_prob: true)
60
+ table_log = FSE_DEFAULT_TABLELOG if table_log.zero?
61
+
62
+ norm = Array.new(max_symbol_value + 1, 0)
63
+
64
+ # RLE: a single symbol accounts for everything.
65
+ (0..max_symbol_value).each do |s|
66
+ return [] if count[s] == total
87
67
  end
88
68
 
89
- [distribution, accuracy_log]
90
- end
69
+ low_prob_count = use_low_prob ? -1 : 1
70
+ scale = 62 - table_log
71
+ step = (1 << 62) / total
72
+ v_step = 1 << (scale - 20)
73
+ still_to_distribute = 1 << table_log
74
+ low_threshold = total >> table_log
75
+ largest = 0
76
+ largest_p = 0
77
+
78
+ (0..max_symbol_value).each do |s|
79
+ if count[s].zero?
80
+ norm[s] = 0
81
+ next
82
+ end
91
83
 
92
- # Calculate minimum accuracy log for given number of symbols
93
- def self.calculate_min_accuracy_log(num_symbols)
94
- return 0 if num_symbols <= 1
84
+ c64 = count[s]
85
+ if c64 <= low_threshold
86
+ norm[s] = low_prob_count
87
+ still_to_distribute -= 1
88
+ else
89
+ proba = (c64 * step) >> scale
90
+ if proba < 8
91
+ rest_to_beat = v_step * RTB_TABLE[proba]
92
+ diff = (c64 * step) - (proba << scale)
93
+ proba += 1 if diff > rest_to_beat
94
+ end
95
+ if proba > largest_p
96
+ largest_p = proba
97
+ largest = s
98
+ end
99
+ norm[s] = proba
100
+ still_to_distribute -= proba
101
+ end
102
+ end
95
103
 
96
- log = 0
97
- temp = num_symbols - 1
98
- while temp.positive?
99
- log += 1
100
- temp >>= 1
104
+ if -still_to_distribute >= norm[largest] / 2
105
+ normalize_m2(norm, table_log, count, total, max_symbol_value,
106
+ low_prob_count)
107
+ else
108
+ norm[largest] += still_to_distribute
101
109
  end
102
- log
110
+
111
+ norm
103
112
  end
104
113
 
105
- # Normalize frequencies to fit table size
106
- def self.normalize_frequencies(frequencies, table_size)
107
- total = frequencies.sum
108
- return Array.new(frequencies.length, 0) if total.zero?
114
+ # Secondary normalization (C FSE_normalizeM2), used when the
115
+ # primary method's largest-symbol correction would be too big.
116
+ # rubocop:disable Metrics/MethodLength
117
+ # rubocop:disable-next Metrics/AbcSize
118
+ def self.normalize_m2(norm, table_log, count, total, max_symbol_value,
119
+ low_prob_count)
120
+ table_size = 1 << table_log
121
+ low_threshold = total >> table_log
122
+ low_one = (total * 3) >> (table_log + 1)
123
+ distributed = 0
124
+ remaining = total
125
+
126
+ (0..max_symbol_value).each do |s|
127
+ if count[s].zero?
128
+ norm[s] = 0
129
+ elsif count[s] <= low_threshold
130
+ norm[s] = low_prob_count
131
+ distributed += 1
132
+ remaining -= count[s]
133
+ elsif count[s] <= low_one
134
+ norm[s] = 1
135
+ distributed += 1
136
+ remaining -= count[s]
137
+ else
138
+ norm[s] = NOT_YET_ASSIGNED
139
+ end
140
+ end
109
141
 
110
- # Scale frequencies
111
- frequencies.map do |freq|
112
- next 0 if freq.nil? || freq <= 0
142
+ to_distribute = table_size - distributed
143
+ return if to_distribute.zero?
113
144
 
114
- normalized = ((freq * table_size) + (total / 2)) / total
115
- [normalized, 1].max # Minimum 1 for non-zero symbols
145
+ if remaining / to_distribute > low_one
146
+ low_one = (remaining * 3) / (to_distribute * 2)
147
+ (0..max_symbol_value).each do |s|
148
+ if norm[s] == NOT_YET_ASSIGNED && count[s] <= low_one
149
+ norm[s] = 1
150
+ distributed += 1
151
+ remaining -= count[s]
152
+ end
153
+ end
154
+ to_distribute = table_size - distributed
116
155
  end
117
- end
118
156
 
119
- # Adjust distribution to sum to exactly table_size
120
- def self.adjust_distribution(distribution, delta)
121
- return if delta.zero?
122
-
123
- if delta.positive?
124
- # Need to add: increment largest probabilities
125
- delta.times do
126
- max_idx = distribution.each_with_index.max_by { |v, _| v }&.last
127
- distribution[max_idx] += 1 if max_idx
157
+ if distributed == max_symbol_value + 1
158
+ max_v = 0
159
+ max_c = 0
160
+ (0..max_symbol_value).each do |s|
161
+ if count[s] > max_c
162
+ max_v = s
163
+ max_c = count[s]
164
+ end
128
165
  end
129
- else
130
- # Need to subtract: decrement smallest non-zero probabilities
131
- (-delta).times do
132
- min_idx = distribution.each_with_index.select do |v, _|
133
- v > 1
134
- end.min_by { |v, _| v }&.last
135
- distribution[min_idx] -= 1 if min_idx
166
+ norm[max_v] += to_distribute
167
+ return
168
+ end
169
+
170
+ if remaining.zero?
171
+ idx = 0
172
+ while to_distribute.positive? && idx <= max_symbol_value
173
+ if norm[idx]&.positive?
174
+ norm[idx] += 1
175
+ to_distribute -= 1
176
+ end
177
+ idx = (idx + 1) % (max_symbol_value + 1)
136
178
  end
179
+ return
180
+ end
181
+
182
+ v_step_log = 62 - table_log
183
+ mid = (1 << (v_step_log - 1)) - 1
184
+ r_step = (((1 << v_step_log) * to_distribute) + mid) / remaining
185
+ tmp_total = mid
186
+ (0..max_symbol_value).each do |s|
187
+ next unless norm[s] == NOT_YET_ASSIGNED
188
+
189
+ end_v = tmp_total + (count[s] * r_step)
190
+ weight = (end_v >> v_step_log) - (tmp_total >> v_step_log)
191
+ norm[s] = weight
192
+ tmp_total = end_v
137
193
  end
138
194
  end
195
+ # rubocop:enable Metrics/MethodLength
139
196
 
140
- # Initialize FSE encoder
197
+ # Choose an accuracy log for the given source size and alphabet.
141
198
  #
142
- # @param distribution [Array<Integer>] Normalized symbol distribution
143
- # @param accuracy_log [Integer] Accuracy log
144
- def initialize(distribution, accuracy_log)
145
- @distribution = distribution
146
- @accuracy_log = accuracy_log
147
- @table_size = 1 << accuracy_log
199
+ # Direct port of C FSE_optimalTableLog; the branch ladder is
200
+ # inherent to the algorithm.
201
+ # rubocop:disable-next Metrics/AbcSize
202
+ def self.optimal_table_log(max_table_log, src_size, max_symbol_value)
203
+ return FSE_MIN_ACCURACY_LOG if src_size <= 1
204
+
205
+ max_bits_src = (src_size - 1).bit_length - 1 - 2
206
+ max_bits_src = 0 if max_bits_src.negative?
207
+ min_bits_src = src_size.bit_length + 1
208
+ min_bits_sym = [max_symbol_value, 1].max.bit_length + 2
209
+ min_bits = [min_bits_src, min_bits_sym].min
210
+
211
+ table_log = max_table_log
212
+ table_log = FSE_DEFAULT_TABLELOG if table_log.zero?
213
+ table_log = max_bits_src if max_bits_src < table_log
214
+ table_log = min_bits if min_bits > table_log
215
+ table_log.clamp(FSE_MIN_ACCURACY_LOG, FSE_MAX_ACCURACY_LOG)
216
+ end
148
217
 
149
- # Build encoding tables
218
+ # Build from raw symbols: normalize + store.
219
+ #
220
+ # @param symbols [Array<Integer>]
221
+ # @param max_symbol_value [Integer]
222
+ # @param max_table_log [Integer]
223
+ # @return [Encoder, nil] nil for an RLE stream (single symbol)
224
+ def self.build_from_symbols(symbols, max_symbol_value,
225
+ max_table_log = FSE_DEFAULT_TABLELOG)
226
+ counts = Array.new(max_symbol_value + 1, 0)
227
+ symbols.each { |s| counts[s] += 1 }
228
+ total = symbols.length
229
+
230
+ actual_max = max_symbol_value
231
+ actual_max -= 1 while actual_max.positive? && counts[actual_max].zero?
232
+
233
+ table_log = optimal_table_log(max_table_log, total, actual_max)
234
+ norm = normalize_count(table_log, counts, total, actual_max,
235
+ use_low_prob: true)
236
+ return nil if norm.empty?
237
+
238
+ new(norm, table_log, actual_max)
239
+ end
240
+
241
+ def initialize(distribution, table_log, max_symbol_value)
242
+ @distribution = distribution
243
+ @table_log = table_log
244
+ @max_symbol_value = max_symbol_value
150
245
  build_encoding_tables
151
246
  end
152
247
 
153
- # Encode symbols to bitstream
248
+ # Serialize the table description (NCount) per RFC 8878 §4.1.1.
154
249
  #
155
- # @param symbols [Array<Integer>] Symbols to encode
156
- # @return [String] Encoded bitstream
157
- def encode(symbols)
158
- return "" if symbols.nil? || symbols.empty?
159
-
160
- # Initialize state from last symbol (reverse order encoding)
161
- bitstream = []
162
-
163
- # Encode in reverse order
164
- state = @table_size - 1 # Initial state
165
-
166
- symbols.reverse_each.with_index do |symbol, _idx|
167
- entry = @symbol_to_state[symbol]
168
- next unless entry
169
-
170
- # Find state for this symbol
171
- state = find_state_for_symbol(symbol, state)
172
-
173
- # Output bits for state transition
174
- num_bits = entry[:num_bits]
175
- if num_bits.positive?
176
- # Write lower num_bits of state
177
- mask = (1 << num_bits) - 1
178
- bits_to_write = state & mask
179
- write_bits(bitstream, bits_to_write, num_bits)
180
- state >>= num_bits
250
+ # @return [String]
251
+ # rubocop:disable Metrics/MethodLength
252
+ # rubocop:disable-next Metrics/AbcSize
253
+ def write_ncount
254
+ out = []
255
+ table_size = 1 << @table_log
256
+ bit_stream = 0
257
+ bit_count = 0
258
+ symbol = 0
259
+ alphabet_size = @max_symbol_value + 1
260
+ previous_is_zero = false
261
+ remaining = table_size + 1
262
+ threshold = table_size
263
+ nb_bits = @table_log + 1
264
+
265
+ bit_stream |= (@table_log - FSE_MIN_ACCURACY_LOG) << bit_count
266
+ bit_count += 4
267
+
268
+ while symbol < alphabet_size && remaining > 1
269
+ if previous_is_zero
270
+ start = symbol
271
+ symbol += 1 while symbol < alphabet_size &&
272
+ @distribution[symbol].zero?
273
+ if symbol == alphabet_size
274
+ raise Omnizip::CompressionError, "bad FSE distribution"
275
+ end
276
+
277
+ while symbol >= start + 24
278
+ start += 24
279
+ bit_stream |= 0xFFFF << bit_count
280
+ out.push(bit_stream & 0xFF, (bit_stream >> 8) & 0xFF)
281
+ bit_stream >>= 16
282
+ end
283
+ while symbol >= start + 3
284
+ start += 3
285
+ bit_stream |= 3 << bit_count
286
+ bit_count += 2
287
+ end
288
+ bit_stream |= (symbol - start) << bit_count
289
+ bit_count += 2
290
+ if bit_count > 16
291
+ out.push(bit_stream & 0xFF, (bit_stream >> 8) & 0xFF)
292
+ bit_stream >>= 16
293
+ bit_count -= 16
294
+ end
295
+ end
296
+
297
+ count = @distribution[symbol]
298
+ symbol += 1
299
+ max = ((2 * threshold) - 1) - remaining
300
+ remaining -= count.negative? ? -count : count
301
+ count_val = count + 1
302
+ count_val += max if count_val >= threshold
303
+
304
+ bit_stream |= count_val << bit_count
305
+ bit_count += nb_bits
306
+ bit_count -= 1 if count_val < max
307
+
308
+ previous_is_zero = count_val == 1
309
+ raise Omnizip::CompressionError, "FSE NCount remaining < 1" if remaining < 1
310
+
311
+ while remaining < threshold
312
+ nb_bits -= 1
313
+ threshold >>= 1
314
+ end
315
+
316
+ if bit_count > 16
317
+ out.push(bit_stream & 0xFF, (bit_stream >> 8) & 0xFF)
318
+ bit_stream >>= 16
319
+ bit_count -= 16
181
320
  end
182
321
  end
183
322
 
184
- # Write final state
185
- write_bits(bitstream, state, @accuracy_log)
323
+ unless remaining == 1
324
+ raise Omnizip::CompressionError,
325
+ "FSE NCount remaining != 1 (#{remaining})"
326
+ end
186
327
 
187
- # Convert bit array to bytes (in reverse for FSE)
188
- bits_to_bytes(bitstream.reverse)
328
+ if bit_count.positive?
329
+ n_bytes = (bit_count + 7) / 8
330
+ out.push(*Array.new(n_bytes) { |i| (bit_stream >> (8 * i)) & 0xFF })
331
+ end
332
+
333
+ out.pack("C*")
189
334
  end
335
+ # rubocop:enable Metrics/MethodLength
190
336
 
191
- # Get number of symbols in distribution
337
+ # Encode `symbols` into a 2-state interleaved reverse
338
+ # bitstream (C FSE_compress_usingCTable).
192
339
  #
193
- # @return [Integer]
194
- def symbol_count
195
- @distribution.length
196
- end
340
+ # @param symbols [Array<Integer>]
341
+ # @return [String] bitstream bytes ending with the 1-bit mark
342
+ # Direct port of C FSE_compress_usingCTable.
343
+ # rubocop:disable Metrics/AbcSize
344
+ def compress_symbols(symbols)
345
+ return "" if symbols.length <= 2
346
+
347
+ bitc = BitCStream.new
348
+ ip = symbols.length
349
+
350
+ ip -= 1
351
+ if symbols.length.odd?
352
+ s1 = CState.init2(self, symbols[ip])
353
+ ip -= 1
354
+ s2 = CState.init2(self, symbols[ip])
355
+ ip -= 1
356
+ s1.encode(bitc, self, symbols[ip])
357
+ bitc.flush
358
+ else
359
+ s2 = CState.init2(self, symbols[ip])
360
+ ip -= 1
361
+ s1 = CState.init2(self, symbols[ip])
362
+ end
363
+ # rubocop:enable Metrics/AbcSize
197
364
 
198
- private
365
+ while ip.positive?
366
+ ip -= 1
367
+ s2.encode(bitc, self, symbols[ip])
368
+ break if ip.zero?
199
369
 
200
- # Build encoding tables from distribution
201
- def build_encoding_tables
202
- @symbol_to_state = {}
203
- @state_to_symbol = Array.new(@table_size)
370
+ ip -= 1
371
+ s1.encode(bitc, self, symbols[ip])
372
+ bitc.flush
373
+ end
204
374
 
205
- # Allocate states to symbols based on distribution
206
- position = 0
207
- step = (@table_size >> 1) + (@table_size >> 3) + 3
208
- mask = @table_size - 1
375
+ s2.flush(bitc)
376
+ s1.flush(bitc)
377
+ bitc.close
378
+ end
379
+
380
+ # Serialize table description + bitstream for `symbols`.
381
+ #
382
+ # @return [String]
383
+ def compress(symbols)
384
+ write_ncount + compress_symbols(symbols)
385
+ end
209
386
 
210
- @distribution.each_with_index do |prob, symbol|
211
- next if prob.nil? || prob <= 0
387
+ # @return [Array<Integer>] state transition table
388
+ attr_reader :state_table
212
389
 
213
- # Calculate number of bits for this symbol
214
- num_bits = [@accuracy_log - log2_int(prob), 0].max
390
+ # @return [Array<SymbolTT>]
391
+ attr_reader :symbol_tt
215
392
 
216
- # Allocate states
217
- prob.times do
218
- # Find empty position using spread
219
- while @state_to_symbol[position]
220
- position = (position + step) & mask
221
- end
393
+ private
394
+
395
+ # Direct port of C FSE_buildCTable.
396
+ # rubocop:disable Metrics/AbcSize
397
+ def build_encoding_tables
398
+ table_size = 1 << @table_log
399
+ step = (table_size >> 1) + (table_size >> 3) + 3
400
+ mask = table_size - 1
401
+ max_sv1 = @max_symbol_value + 1
402
+
403
+ table_symbol = Array.new(table_size, 0xFFFF)
404
+ high_threshold = table_size - 1
405
+
406
+ cumul = Array.new(max_sv1 + 1, 0)
407
+ (1..max_sv1).each do |u|
408
+ if @distribution[u - 1] == -1
409
+ cumul[u] = cumul[u - 1] + 1
410
+ table_symbol[high_threshold] = u - 1
411
+ high_threshold -= 1
412
+ else
413
+ cumul[u] = cumul[u - 1] + @distribution[u - 1]
414
+ end
415
+ # rubocop:enable Metrics/AbcSize
416
+ end
417
+ cumul[max_sv1] = table_size + 1
222
418
 
223
- @state_to_symbol[position] = {
224
- symbol: symbol,
225
- num_bits: num_bits,
226
- baseline: 0, # Will be calculated
227
- }
419
+ position = 0
420
+ @distribution.each_with_index do |freq, symbol|
421
+ next unless freq.positive?
228
422
 
423
+ freq.times do
424
+ table_symbol[position] = symbol
229
425
  position = (position + step) & mask
426
+ position = (position + step) & mask while position > high_threshold
230
427
  end
428
+ end
231
429
 
232
- @symbol_to_state[symbol] = {
233
- num_bits: num_bits,
234
- baseline: 0,
235
- }
430
+ @state_table = Array.new(table_size, 0)
431
+ table_size.times do |u|
432
+ s = table_symbol[u]
433
+ @state_table[cumul[s]] = table_size + u
434
+ cumul[s] += 1
236
435
  end
237
436
 
238
- # Calculate baselines
239
- calculate_baselines
437
+ @symbol_tt = Array.new(max_sv1) { SymbolTT.new(0, 0) }
438
+ total = 0
439
+ max_sv1.times do |s|
440
+ case @distribution[s]
441
+ when 0
442
+ @symbol_tt[s].delta_nb_bits = ((@table_log + 1) << 16) -
443
+ (1 << @table_log)
444
+ when -1, 1
445
+ @symbol_tt[s].delta_nb_bits = (@table_log << 16) -
446
+ (1 << @table_log)
447
+ @symbol_tt[s].delta_find_state = total - 1
448
+ total += 1
449
+ else
450
+ n = @distribution[s]
451
+ max_bits_out = @table_log - Constants.highbit32(n - 1)
452
+ min_state_plus = n << max_bits_out
453
+ @symbol_tt[s].delta_nb_bits = (max_bits_out << 16) - min_state_plus
454
+ @symbol_tt[s].delta_find_state = total - n
455
+ total += n
456
+ end
457
+ end
240
458
  end
241
459
 
242
- # Calculate baseline values for each state
243
- def calculate_baselines
244
- # Group states by symbol
245
- symbol_states = {}
246
- @state_to_symbol.each_with_index do |entry, state|
247
- next unless entry
460
+ # Forward bit writer (C BIT_CStream): accumulates at the low
461
+ # end and flushes whole bytes.
462
+ class BitCStream
463
+ def initialize
464
+ @container = 0
465
+ @bit_pos = 0
466
+ end
248
467
 
249
- symbol = entry[:symbol]
250
- symbol_states[symbol] ||= []
251
- symbol_states[symbol] << { state: state, entry: entry }
468
+ # @param value [Integer] bits in the low end
469
+ # @param nb_bits [Integer] 0..31
470
+ def add_bits(value, nb_bits)
471
+ mask = (1 << nb_bits) - 1
472
+ @container |= (value & mask) << @bit_pos
473
+ @bit_pos += nb_bits
252
474
  end
253
475
 
254
- # Sort states within each symbol and assign baselines
255
- symbol_states.each_value do |states|
256
- states.sort_by! { |s| s[:state] }
257
- states.each_with_index do |s, idx|
258
- s[:entry][:baseline] = idx
476
+ def flush
477
+ nb_bytes = @bit_pos >> 3
478
+ @bytes ||= []
479
+ nb_bytes.times do |i|
480
+ @bytes << ((@container >> (8 * i)) & 0xFF)
259
481
  end
482
+ @container >>= nb_bytes * 8
483
+ @bit_pos &= 7
260
484
  end
261
- end
262
485
 
263
- # Find state for encoding a symbol
264
- def find_state_for_symbol(symbol, current_state)
265
- entry = @symbol_to_state[symbol]
266
- return 0 unless entry
267
-
268
- # Find the appropriate state based on current state
269
- num_bits = entry[:num_bits]
270
- if num_bits.positive?
271
- # Use lower bits of current state to select state
272
- ((current_state & ((1 << num_bits) - 1)) << (@accuracy_log - num_bits)) |
273
- (entry[:baseline] >> num_bits)
274
- else
275
- entry[:baseline]
486
+ # Add the 1 end-mark bit and flush everything.
487
+ #
488
+ # @return [String]
489
+ def close
490
+ add_bits(1, 1)
491
+ flush
492
+ @bytes << (@container & 0xFF) if @bit_pos.positive?
493
+ @bytes.pack("C*")
276
494
  end
277
495
  end
278
496
 
279
- # Write bits to bitstream array
280
- def write_bits(bitstream, value, count)
281
- count.times do |i|
282
- bitstream << ((value >> i) & 1)
283
- end
284
- end
497
+ # FSE encoder state (value in [table_size, 2*table_size)).
498
+ class CState
499
+ attr_reader :value
285
500
 
286
- # Convert bit array to bytes
287
- def bits_to_bytes(bits)
288
- # Pad to byte boundary
289
- bits = bits.dup
290
- while bits.length % 8 != 0
291
- bits << 0
501
+ def initialize(value, state_log)
502
+ @value = value
503
+ @state_log = state_log
292
504
  end
293
505
 
294
- bytes = []
295
- bits.each_slice(8) do |byte_bits|
296
- byte = 0
297
- byte_bits.each_with_index do |bit, i|
298
- byte |= (bit << i)
299
- end
300
- bytes << byte
506
+ # Initialize to the baseline for the first symbol to encode
507
+ # (the last to decode); C FSE_initCState2.
508
+ # rubocop:disable-next Metrics/AbcSize
509
+ def self.init2(table, symbol)
510
+ s_tt = table.symbol_tt[symbol]
511
+ nb_bits_out = (s_tt.delta_nb_bits + (1 << 15)) >> 16
512
+ value = (nb_bits_out << 16) - s_tt.delta_nb_bits
513
+ idx = (value >> nb_bits_out) + s_tt.delta_find_state
514
+ new(table.state_table[idx], table.table_log)
301
515
  end
302
516
 
303
- bytes.pack("C*")
304
- end
305
-
306
- # Integer log2
307
- def log2_int(value)
308
- return 0 if value <= 1
517
+ def encode(bitc, table, symbol)
518
+ s_tt = table.symbol_tt[symbol]
519
+ nb_bits_out = (@value + s_tt.delta_nb_bits) >> 16
520
+ bitc.add_bits(@value, nb_bits_out)
521
+ idx = (@value >> nb_bits_out) + s_tt.delta_find_state
522
+ @value = table.state_table[idx]
523
+ end
309
524
 
310
- log = 0
311
- temp = value
312
- while temp > 1
313
- log += 1
314
- temp >>= 1
525
+ def flush(bitc)
526
+ bitc.add_bits(@value, @state_log)
527
+ bitc.flush
315
528
  end
316
- log
317
529
  end
318
530
  end
319
531
  end