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
@@ -0,0 +1,92 @@
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
+ module FSE
27
+ # 2-state interleaved FSE stream decoder (RFC 8878 §4.2.1.2,
28
+ # "FSE compression of Huffman weights"; mirrors the C reference
29
+ # FSE_decompress_usingDTable).
30
+ #
31
+ # Symbols are produced until the bitstream overflows; after an
32
+ # overflow following one state's transition, one final symbol is
33
+ # decoded from the other state without transitioning.
34
+ module Interleaved
35
+ module_function
36
+
37
+ # Decode a 2-state interleaved FSE stream.
38
+ #
39
+ # @param table [Table]
40
+ # @param bitstream_bytes [String]
41
+ # @param max_output [Integer] cap on decoded length (corrupt
42
+ # input protection)
43
+ # @return [Array<Integer>] decoded symbols
44
+ def decode_stream(table, bitstream_bytes, max_output)
45
+ bs = BitStream.new(bitstream_bytes)
46
+ accuracy_log = table.accuracy_log
47
+
48
+ s1 = bs.read_bits(accuracy_log)
49
+ bs.reload
50
+ s2 = bs.read_bits(accuracy_log)
51
+ bs.reload
52
+
53
+ out = []
54
+
55
+ loop do
56
+ check_length!(out, max_output)
57
+ e1 = table[s1]
58
+ out << e1.symbol
59
+ s1 = e1.baseline + bs.read_bits(e1.num_bits)
60
+
61
+ if bs.reload_status == BitStream::OVERFLOW
62
+ check_length!(out, max_output)
63
+ out << table[s2].symbol
64
+ break
65
+ end
66
+
67
+ check_length!(out, max_output)
68
+ e2 = table[s2]
69
+ out << e2.symbol
70
+ s2 = e2.baseline + bs.read_bits(e2.num_bits)
71
+
72
+ if bs.reload_status == BitStream::OVERFLOW
73
+ check_length!(out, max_output)
74
+ out << table[s1].symbol
75
+ break
76
+ end
77
+ end
78
+
79
+ out
80
+ end
81
+
82
+ def check_length!(out, max_output)
83
+ return unless out.length >= max_output
84
+
85
+ raise Omnizip::DecompressionError,
86
+ "FSE decode exceeded max output #{max_output}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -24,240 +24,162 @@ module Omnizip
24
24
  module Algorithms
25
25
  class Zstandard
26
26
  module FSE
27
- # FSE state entry for decoding table
28
- #
29
- # Each entry contains:
30
- # - symbol: The symbol this state decodes to
31
- # - num_bits: Number of bits to read for next state
32
- # - baseline: Value to add to next state's value
33
- State = Struct.new(:symbol, :num_bits, :baseline)
34
-
35
- # FSE decoding table (RFC 8878 Section 4.1)
36
- #
37
- # Builds a decoding table from a probability distribution
38
- # according to RFC 8878.
27
+ # One FSE table entry: the symbol decoded in this state and the
28
+ # transition rule (bits to read, baseline to add). base_val /
29
+ # nb_add_bits carry the sequence-code meaning of the symbol
30
+ # (LL/ML baseline + extra bits, or OF base + offset code width)
31
+ # and are attached by the sequences decoder.
32
+ State = Struct.new(:symbol, :num_bits, :baseline, :base_val,
33
+ :nb_add_bits)
34
+
35
+ # FSE decoding table built from a normalized distribution
36
+ # (RFC 8878 §4.1, "From normalized distribution to decoding
37
+ # tables"; mirrors the C reference FSE_buildDTable).
39
38
  class Table
40
39
  include Constants
41
40
 
42
- # @return [Array<State>] Decoding table entries
41
+ # @return [Array<State>]
43
42
  attr_reader :states
44
43
 
45
- # @return [Integer] Accuracy log (table size = 2^accuracy_log)
44
+ # @return [Integer] log2 of the table size
46
45
  attr_reader :accuracy_log
47
46
 
48
- # @return [Integer] Number of symbols in the table
47
+ # @return [Integer] number of symbols in the source alphabet
49
48
  attr_reader :symbol_count
50
49
 
51
- # Build FSE table from normalized distribution
50
+ # Build a table from a distribution.
52
51
  #
53
- # @param distribution [Array<Integer>] Normalized symbol frequencies
54
- # @param accuracy_log [Integer] Log2 of table size
55
- # @return [Table] Built FSE table
52
+ # @param distribution [Array<Integer>] normalized counts;
53
+ # positive = cell count, -1 = "less than 1" low-probability
54
+ # symbol (one cell at the top of the table), 0 = absent
55
+ # @param accuracy_log [Integer]
56
+ # @return [Table]
56
57
  def self.build(distribution, accuracy_log)
57
58
  table_size = 1 << accuracy_log
59
+ step = (table_size >> 1) + (table_size >> 3) + 3
60
+ mask = table_size - 1
61
+
62
+ # Phase 1a: low-probability (-1) symbols occupy single cells
63
+ # from the top of the table downward.
64
+ table_symbol = Array.new(table_size, 0xFFFF)
65
+ high_threshold = table_size - 1
66
+ distribution.each_with_index do |freq, symbol|
67
+ next unless freq == -1
68
+
69
+ table_symbol[high_threshold] = symbol
70
+ if high_threshold.zero?
71
+ raise Omnizip::DecompressionError,
72
+ "FSE table overflow placing low-probability symbol"
73
+ end
74
+
75
+ high_threshold -= 1
76
+ end
58
77
 
59
- # Allocate cells using spread pattern
60
- cells = allocate_cells(distribution, table_size)
78
+ # Phase 1b: spread positive-count symbols from position 0,
79
+ # skipping the reserved low-probability area.
80
+ position = 0
81
+ distribution.each_with_index do |freq, symbol|
82
+ next unless freq.positive?
83
+
84
+ freq.times do
85
+ table_symbol[position] = symbol
86
+ position = (position + step) & mask
87
+ position = (position + step) & mask while position > high_threshold
88
+ end
89
+ end
61
90
 
62
- # Calculate num_bits and baseline for each state
63
- states = calculate_state_values(cells, distribution, table_size)
91
+ # Phase 2: symbolNext starts at 1 for -1/1 counts, at the
92
+ # count itself otherwise.
93
+ symbol_next = Array.new(distribution.length, 0)
94
+ singular = [-1, 1].freeze
95
+ distribution.each_with_index do |freq, symbol|
96
+ next if freq.zero?
97
+
98
+ symbol_next[symbol] = singular.include?(freq) ? 1 : freq
99
+ end
100
+
101
+ # Phase 3: per-cell transition rules.
102
+ states = table_symbol.map do |symbol|
103
+ next_state = symbol_next[symbol]
104
+ symbol_next[symbol] += 1
105
+ nb_bits = accuracy_log - Constants.highbit32(next_state)
106
+ baseline = (next_state << nb_bits) - table_size
107
+ State.new(symbol, nb_bits, baseline)
108
+ end
64
109
 
65
110
  new(states, accuracy_log, distribution.length)
66
111
  end
67
112
 
68
- # Build from predefined distribution
113
+ # Build from an RFC 8878 §4.1.3 predefined distribution.
69
114
  #
70
- # @param distribution [Array<Integer>] Predefined distribution
71
- # @param accuracy_log [Integer] Accuracy log
115
+ # @param distribution [Array<Integer>]
116
+ # @param accuracy_log [Integer]
72
117
  # @return [Table]
73
118
  def self.build_predefined(distribution, accuracy_log)
74
119
  build(distribution, accuracy_log)
75
120
  end
76
121
 
77
- # Initialize with pre-built table
122
+ # Single-symbol RLE table: every state decodes `symbol` with a
123
+ # full state reset.
78
124
  #
79
- # @param states [Array<State>] Decoding states
80
- # @param accuracy_log [Integer]
81
- # @param symbol_count [Integer]
125
+ # @return [Table]
126
+ def self.build_rle(symbol, accuracy_log)
127
+ new(Array.new(1 << accuracy_log) { State.new(symbol, 0, 0) },
128
+ accuracy_log, 1)
129
+ end
130
+
82
131
  def initialize(states, accuracy_log, symbol_count)
83
132
  @states = states
84
133
  @accuracy_log = accuracy_log
85
134
  @symbol_count = symbol_count
86
135
  end
87
136
 
88
- # Get state at index
89
- #
90
- # @param index [Integer] State index
91
- # @return [State] State at index
137
+ # @return [State]
92
138
  def [](index)
93
139
  @states[index]
94
140
  end
95
141
 
96
- # Get table size
97
- #
98
- # @return [Integer] Number of entries in table
142
+ # @return [Integer]
99
143
  def size
100
144
  @states.length
101
145
  end
102
-
103
- # Allocate cells using FSE spread pattern
104
- #
105
- # The spread pattern distributes symbols across the table
106
- # using a step that ensures good distribution.
107
- def self.allocate_cells(distribution, table_size)
108
- cells = Array.new(table_size, nil)
109
-
110
- # Validate distribution sum
111
- total = distribution.compact.sum
112
- if total > table_size
113
- raise ArgumentError,
114
- "Distribution sum (#{total}) exceeds table size (#{table_size})"
115
- end
116
-
117
- # Step = (table_size >> 1) + (table_size >> 3) + 3
118
- step = (table_size >> 1) + (table_size >> 3) + 3
119
- mask = table_size - 1
120
-
121
- position = 0
122
-
123
- distribution.each_with_index do |prob, symbol|
124
- next if prob.nil? || prob <= 0
125
-
126
- prob.times do
127
- # Find empty position (with safety limit)
128
- attempts = 0
129
- while cells[position]
130
- position = (position + step) & mask
131
- attempts += 1
132
- if attempts > table_size
133
- raise "FSE table allocation failed: no empty cell found"
134
- end
135
- end
136
-
137
- cells[position] = symbol
138
- position = (position + step) & mask
139
- end
140
- end
141
-
142
- cells
143
- end
144
-
145
- # Calculate num_bits and baseline for each state
146
- def self.calculate_state_values(cells, distribution, table_size)
147
- states = Array.new(table_size)
148
-
149
- # Group positions by symbol
150
- symbol_positions = {}
151
- cells.each_with_index do |symbol, pos|
152
- next if symbol.nil?
153
-
154
- symbol_positions[symbol] ||= []
155
- symbol_positions[symbol] << pos
156
- end
157
-
158
- # Calculate state values for each symbol
159
- symbol_positions.each do |symbol, positions|
160
- prob = distribution[symbol]
161
- next if prob.nil? || prob <= 0
162
-
163
- positions.each_with_index do |pos, idx|
164
- # Calculate num_bits: -log2(prob/table_size)
165
- num_bits = calculate_num_bits(prob, table_size)
166
-
167
- # Calculate baseline
168
- baseline = idx
169
-
170
- states[pos] = State.new(symbol, num_bits, baseline)
171
- end
172
- end
173
-
174
- states
175
- end
176
-
177
- # Calculate number of bits needed for a symbol with given probability
178
- def self.calculate_num_bits(prob, table_size)
179
- return 0 if prob <= 0
180
-
181
- # num_bits = accuracy_log - log2(prob)
182
- # This is the number of extra bits needed
183
- log_prob = 0
184
- temp = prob
185
- while temp > 1
186
- log_prob += 1
187
- temp >>= 1
188
- end
189
-
190
- log_table = 0
191
- temp = table_size
192
- while temp > 1
193
- log_table += 1
194
- temp >>= 1
195
- end
196
-
197
- [0, log_table - log_prob].max
198
- end
199
146
  end
200
147
 
201
- # FSE Decoder (RFC 8878 Section 4.1)
202
- #
203
- # Decodes symbols from FSE-encoded bitstreams.
148
+ # Stateful single-stream FSE decoder.
204
149
  class Decoder
205
- # @return [Table] FSE decoding table
206
- attr_reader :table
207
-
208
- # @return [Integer] Current state
150
+ # @return [Integer] current state index
209
151
  attr_reader :state
210
152
 
211
- # Initialize decoder with FSE table
212
- #
213
- # @param table [Table] FSE decoding table
153
+ # @param table [Table]
214
154
  def initialize(table)
215
155
  @table = table
216
156
  @state = 0
217
157
  end
218
158
 
219
- # Initialize state from bitstream
220
- #
221
- # @param bitstream [BitStream] The bitstream to read from
159
+ # Initialize the state by reading accuracy_log bits.
222
160
  def init_state(bitstream)
223
161
  @state = bitstream.read_bits(@table.accuracy_log)
224
162
  end
225
163
 
226
- # Decode next symbol from bitstream
164
+ # Decode one symbol: read it from the current state, then
165
+ # transition.
227
166
  #
228
- # @param bitstream [BitStream] The bitstream to read from
229
- # @return [Integer] Decoded symbol
167
+ # @return [Integer] symbol
230
168
  def decode(bitstream)
231
169
  entry = @table[@state]
232
- return 0 if entry.nil?
233
-
234
- symbol = entry.symbol
235
-
236
- # Read extra bits for next state
237
- if entry.num_bits.positive?
238
- extra = bitstream.read_bits(entry.num_bits)
239
- @state = entry.baseline + extra
240
- else
241
- @state = entry.baseline
242
- end
243
-
244
- # Mask state to table size
245
- @state &= (@table.size - 1)
246
-
247
- symbol
170
+ @state = if entry.num_bits.positive?
171
+ entry.baseline + bitstream.read_bits(entry.num_bits)
172
+ else
173
+ entry.baseline
174
+ end
175
+ entry.symbol
248
176
  end
249
177
 
250
- # Decode multiple symbols
178
+ # The symbol the current state decodes, without transitioning.
251
179
  #
252
- # @param bitstream [BitStream] The bitstream to read from
253
- # @param count [Integer] Number of symbols to decode
254
- # @return [Array<Integer>] Decoded symbols
255
- def decode_symbols(bitstream, count)
256
- symbols = []
257
- count.times do
258
- symbols << decode(bitstream)
259
- end
260
- symbols
180
+ # @return [Integer]
181
+ def current_symbol
182
+ @table[@state].symbol
261
183
  end
262
184
  end
263
185
  end
@@ -0,0 +1,203 @@
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
+ module FSE
27
+ # Reads an FSE table description ("NCount") from the wire and
28
+ # builds an FSE decode Table (RFC 8878 §4.1.1, FSE Table
29
+ # Description; mirrors the C reference FSE_readNCount).
30
+ #
31
+ # The distribution is a forward little-endian bit-packed stream:
32
+ # 4 bits of table log, then per-symbol counts whose bit width
33
+ # shrinks as probability points are spent, with 2-bit repeat
34
+ # codes for runs of zero-count symbols.
35
+ module TableDescription
36
+ include Constants
37
+
38
+ MAX_SYMBOL_VALUE = 255
39
+
40
+ # rubocop:disable Metrics/MethodLength
41
+ # rubocop:disable Metrics/PerceivedComplexity
42
+ module_function
43
+
44
+ # Read a normalized distribution from the head of `src`.
45
+ #
46
+ # @param src [String] bytes holding the table description
47
+ # (plus whatever follows)
48
+ # @return [Array(Table, Integer)] the decode table and the
49
+ # number of bytes consumed
50
+ def read(src)
51
+ raise Omnizip::DecompressionError, "FSE table description truncated" if src.bytesize < 4
52
+
53
+ reader = BitReader.new(src)
54
+
55
+ table_log = (reader.peek & 0xF) + FSE_MIN_ACCURACY_LOG
56
+ if table_log > FSE_MAX_ACCURACY_LOG
57
+ raise Omnizip::DecompressionError,
58
+ "FSE tableLog #{table_log} exceeds max #{FSE_MAX_ACCURACY_LOG}"
59
+ end
60
+ reader.skip_bits(4)
61
+
62
+ remaining = (1 << table_log) + 1
63
+ threshold = 1 << table_log
64
+ nb_bits = table_log + 1
65
+
66
+ counts = Array.new(MAX_SYMBOL_VALUE + 1, 0)
67
+ charnum = 0
68
+ previous0 = false
69
+
70
+ loop do
71
+ if previous0
72
+ charnum = skip_zero_run(reader, charnum)
73
+ previous0 = false
74
+ break if charnum > MAX_SYMBOL_VALUE
75
+
76
+ next
77
+ end
78
+
79
+ max = ((2 * threshold) - 1) - remaining
80
+ masked = reader.peek & (threshold - 1)
81
+ if masked < max
82
+ count = masked
83
+ reader.skip_bits(nb_bits - 1)
84
+ else
85
+ count = reader.peek & ((2 * threshold) - 1)
86
+ count -= max if count >= threshold
87
+ reader.skip_bits(nb_bits)
88
+ end
89
+
90
+ actual = count - 1
91
+ actual.negative? ? remaining += actual : remaining -= actual
92
+ counts[charnum] = actual
93
+ charnum += 1
94
+ previous0 = actual.zero?
95
+
96
+ if remaining < threshold
97
+ break if remaining <= 1
98
+
99
+ high = Constants.highbit32(remaining)
100
+ nb_bits = high + 1
101
+ threshold = 1 << high
102
+ end
103
+ break if charnum > MAX_SYMBOL_VALUE
104
+ end
105
+
106
+ if remaining != 1
107
+ raise Omnizip::DecompressionError,
108
+ "FSE distribution does not sum to table size (remaining=#{remaining})"
109
+ end
110
+ if charnum.zero?
111
+ raise Omnizip::DecompressionError, "FSE distribution is empty"
112
+ end
113
+
114
+ counts = counts.first(charnum)
115
+ [Table.build(counts, table_log), reader.bytes_consumed]
116
+ end
117
+ # rubocop:enable Metrics/PerceivedComplexity
118
+ # rubocop:enable Metrics/MethodLength
119
+
120
+ # Count 2-bit repeat codes after a zero-count symbol.
121
+ def skip_zero_run(reader, charnum)
122
+ inverted = (~reader.peek) | 0x80000000
123
+ repeats = trailing_zeros(inverted) >> 1
124
+ while repeats >= 12
125
+ charnum += 36
126
+ charnum = MAX_SYMBOL_VALUE + 1 if charnum > MAX_SYMBOL_VALUE + 1
127
+ reader.skip_bits(24)
128
+ inverted = (~reader.peek) | 0x80000000
129
+ repeats = trailing_zeros(inverted) >> 1
130
+ end
131
+ charnum += 3 * repeats
132
+ charnum = MAX_SYMBOL_VALUE + 1 if charnum > MAX_SYMBOL_VALUE + 1
133
+ reader.skip_bits(2 * repeats)
134
+
135
+ tail = reader.peek & 3
136
+ charnum += tail
137
+ charnum = MAX_SYMBOL_VALUE + 1 if charnum > MAX_SYMBOL_VALUE + 1
138
+ reader.skip_bits(2)
139
+
140
+ charnum
141
+ end
142
+
143
+ def trailing_zeros(value)
144
+ count = 0
145
+ while value.nobits?(1) && count < 32
146
+ value >>= 1
147
+ count += 1
148
+ end
149
+ count
150
+ end
151
+
152
+ # Forward little-endian bit reader over a byte string with a
153
+ # sliding 32-bit window (C FSE_readNCount's bit reader).
154
+ class BitReader
155
+ def initialize(src)
156
+ @src = src
157
+ @byte_pos = 0
158
+ @bit_count = 0
159
+ @bit_window = read_u32(0)
160
+ end
161
+
162
+ def peek
163
+ @bit_window
164
+ end
165
+
166
+ def skip_bits(bits)
167
+ refill(bits)
168
+ end
169
+
170
+ def bytes_consumed
171
+ @byte_pos + ((@bit_count + 7) >> 3)
172
+ end
173
+
174
+ private
175
+
176
+ def refill(extra_bits)
177
+ @bit_count += extra_bits
178
+ advance = @bit_count >> 3
179
+ if @byte_pos + advance + 4 > @src.bytesize
180
+ # Clamp to the last 4 bytes, as the C reference does.
181
+ new_pos = @src.bytesize - 4
182
+ consumed = (new_pos - @byte_pos) * 8
183
+ @bit_count = (((@bit_count - consumed) % 32) + 32) % 32
184
+ @byte_pos = new_pos
185
+ else
186
+ @byte_pos += advance
187
+ @bit_count &= 7
188
+ end
189
+ @bit_window = read_u32(@byte_pos) >> @bit_count
190
+ end
191
+
192
+ def read_u32(offset)
193
+ @src.getbyte(offset) |
194
+ (@src.getbyte(offset + 1) << 8) |
195
+ (@src.getbyte(offset + 2) << 16) |
196
+ (@src.getbyte(offset + 3) << 24)
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -23,11 +23,30 @@
23
23
  module Omnizip
24
24
  module Algorithms
25
25
  class Zstandard
26
- # FSE (Finite State Entropy) module namespace
26
+ # FSE (Finite State Entropy) namespace (RFC 8878 §4.1).
27
27
  module FSE
28
28
  autoload :BitStream, "omnizip/algorithms/zstandard/fse/bitstream"
29
- autoload :Encoder, "omnizip/algorithms/zstandard/fse/encoder"
30
29
  autoload :Table, "omnizip/algorithms/zstandard/fse/table"
30
+ autoload :TableDescription,
31
+ "omnizip/algorithms/zstandard/fse/table_description"
32
+ autoload :Interleaved, "omnizip/algorithms/zstandard/fse/interleaved"
33
+ autoload :Encoder, "omnizip/algorithms/zstandard/fse/encoder"
34
+
35
+ module_function
36
+
37
+ # Read an FSE table description from the head of `src`.
38
+ #
39
+ # @return [Array(Table, Integer)] table and bytes consumed
40
+ def read_table(src)
41
+ TableDescription.read(src)
42
+ end
43
+
44
+ # Decode a 2-state interleaved FSE stream.
45
+ #
46
+ # @return [Array<Integer>]
47
+ def decode_stream(table, bitstream_bytes, max_output)
48
+ Interleaved.decode_stream(table, bitstream_bytes, max_output)
49
+ end
31
50
  end
32
51
  end
33
52
  end