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.
- checksums.yaml +4 -4
- data/lib/omnizip/algorithm.rb +36 -1
- data/lib/omnizip/algorithms/bzip2.rb +1 -3
- data/lib/omnizip/algorithms/deflate.rb +1 -3
- data/lib/omnizip/algorithms/deflate64.rb +2 -2
- data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +7 -8
- data/lib/omnizip/algorithms/lzma.rb +1 -1
- data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +5 -1
- data/lib/omnizip/algorithms/lzma2.rb +4 -12
- data/lib/omnizip/algorithms/zstandard/constants.rb +38 -25
- data/lib/omnizip/algorithms/zstandard/decoder.rb +101 -136
- data/lib/omnizip/algorithms/zstandard/encoder.rb +81 -138
- data/lib/omnizip/algorithms/zstandard/frame/block.rb +17 -0
- data/lib/omnizip/algorithms/zstandard/frame/header.rb +42 -29
- data/lib/omnizip/algorithms/zstandard/fse/bitstream.rb +114 -117
- data/lib/omnizip/algorithms/zstandard/fse/encoder.rb +434 -222
- data/lib/omnizip/algorithms/zstandard/fse/interleaved.rb +92 -0
- data/lib/omnizip/algorithms/zstandard/fse/table.rb +98 -176
- data/lib/omnizip/algorithms/zstandard/fse/table_description.rb +203 -0
- data/lib/omnizip/algorithms/zstandard/fse.rb +21 -2
- data/lib/omnizip/algorithms/zstandard/huffman.rb +195 -188
- data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +232 -255
- data/lib/omnizip/algorithms/zstandard/literals.rb +170 -103
- data/lib/omnizip/algorithms/zstandard/literals_encoder.rb +47 -199
- data/lib/omnizip/algorithms/zstandard/sequences.rb +260 -252
- data/lib/omnizip/algorithms/zstandard/xxhash.rb +159 -0
- data/lib/omnizip/algorithms/zstandard.rb +20 -24
- data/lib/omnizip/algorithms.rb +14 -1
- data/lib/omnizip/buffer.rb +1 -2
- data/lib/omnizip/commands/archive_list_command.rb +9 -7
- data/lib/omnizip/convenience.rb +1 -2
- data/lib/omnizip/entry.rb +44 -0
- data/lib/omnizip/extraction/selective_extractor.rb +2 -10
- data/lib/omnizip/filter_pipeline.rb +1 -1
- data/lib/omnizip/filter_registry.rb +18 -16
- data/lib/omnizip/filters/bcj2.rb +1 -1
- data/lib/omnizip/filters/bcj_arm.rb +1 -1
- data/lib/omnizip/filters/bcj_arm64.rb +1 -1
- data/lib/omnizip/filters/bcj_ia64.rb +1 -1
- data/lib/omnizip/filters/bcj_ppc.rb +1 -1
- data/lib/omnizip/filters/bcj_sparc.rb +1 -1
- data/lib/omnizip/filters/bcj_x86.rb +1 -1
- data/lib/omnizip/filters.rb +0 -2
- data/lib/omnizip/formats/bzip2_file.rb +0 -7
- data/lib/omnizip/formats/cpio/entry.rb +6 -0
- data/lib/omnizip/formats/cpio.rb +0 -6
- data/lib/omnizip/formats/gzip.rb +0 -7
- data/lib/omnizip/formats/iso/directory_record.rb +7 -0
- data/lib/omnizip/formats/iso.rb +0 -5
- data/lib/omnizip/formats/lzip.rb +0 -7
- data/lib/omnizip/formats/lzma_alone.rb +0 -6
- data/lib/omnizip/formats/msi/entry.rb +6 -0
- data/lib/omnizip/formats/msi.rb +0 -9
- data/lib/omnizip/formats/ole/dirent.rb +6 -0
- data/lib/omnizip/formats/ole.rb +0 -9
- data/lib/omnizip/formats/rar.rb +0 -6
- data/lib/omnizip/formats/rar3/reader.rb +7 -0
- data/lib/omnizip/formats/rar5/reader.rb +7 -0
- data/lib/omnizip/formats/rpm/entry.rb +6 -0
- data/lib/omnizip/formats/seven_zip/models/file_entry.rb +7 -0
- data/lib/omnizip/formats/seven_zip.rb +0 -6
- data/lib/omnizip/formats/tar/entry.rb +6 -0
- data/lib/omnizip/formats/tar.rb +2 -10
- data/lib/omnizip/formats/xar/entry.rb +6 -0
- data/lib/omnizip/formats/xar.rb +0 -6
- data/lib/omnizip/formats/zip.rb +0 -6
- data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +33 -31
- data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +177 -163
- data/lib/omnizip/implementations.rb +18 -4
- data/lib/omnizip/io/source.rb +3 -0
- data/lib/omnizip/metadata/entry_metadata.rb +7 -0
- data/lib/omnizip/parallel/engine.rb +48 -0
- data/lib/omnizip/parallel/parallel_compressor.rb +5 -16
- data/lib/omnizip/parallel/parallel_extractor.rb +5 -16
- data/lib/omnizip/parallel.rb +1 -0
- data/lib/omnizip/profile/profile_registry.rb +1 -2
- data/lib/omnizip/registry.rb +0 -1
- data/lib/omnizip/rubyzip_compat.rb +0 -1
- data/lib/omnizip/temp/temp_file.rb +1 -2
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip/zip/entry.rb +7 -0
- data/lib/omnizip/zip/file.rb +4 -8
- data/lib/omnizip.rb +1 -1
- metadata +7 -6
- data/lib/omnizip/filters/filter_base.rb +0 -6
- data/lib/omnizip/filters/registration.rb +0 -22
- data/lib/omnizip/filters/registry.rb +0 -111
- 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
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# FSE decoding table
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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>]
|
|
41
|
+
# @return [Array<State>]
|
|
43
42
|
attr_reader :states
|
|
44
43
|
|
|
45
|
-
# @return [Integer]
|
|
44
|
+
# @return [Integer] log2 of the table size
|
|
46
45
|
attr_reader :accuracy_log
|
|
47
46
|
|
|
48
|
-
# @return [Integer]
|
|
47
|
+
# @return [Integer] number of symbols in the source alphabet
|
|
49
48
|
attr_reader :symbol_count
|
|
50
49
|
|
|
51
|
-
# Build
|
|
50
|
+
# Build a table from a distribution.
|
|
52
51
|
#
|
|
53
|
-
# @param distribution [Array<Integer>]
|
|
54
|
-
#
|
|
55
|
-
#
|
|
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
|
-
#
|
|
60
|
-
|
|
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
|
-
#
|
|
63
|
-
|
|
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>]
|
|
71
|
-
# @param accuracy_log [Integer]
|
|
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
|
-
#
|
|
122
|
+
# Single-symbol RLE table: every state decodes `symbol` with a
|
|
123
|
+
# full state reset.
|
|
78
124
|
#
|
|
79
|
-
# @
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
202
|
-
#
|
|
203
|
-
# Decodes symbols from FSE-encoded bitstreams.
|
|
148
|
+
# Stateful single-stream FSE decoder.
|
|
204
149
|
class Decoder
|
|
205
|
-
# @return [
|
|
206
|
-
attr_reader :table
|
|
207
|
-
|
|
208
|
-
# @return [Integer] Current state
|
|
150
|
+
# @return [Integer] current state index
|
|
209
151
|
attr_reader :state
|
|
210
152
|
|
|
211
|
-
#
|
|
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
|
|
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
|
|
164
|
+
# Decode one symbol: read it from the current state, then
|
|
165
|
+
# transition.
|
|
227
166
|
#
|
|
228
|
-
# @
|
|
229
|
-
# @return [Integer] Decoded symbol
|
|
167
|
+
# @return [Integer] symbol
|
|
230
168
|
def decode(bitstream)
|
|
231
169
|
entry = @table[@state]
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
#
|
|
178
|
+
# The symbol the current state decodes, without transitioning.
|
|
251
179
|
#
|
|
252
|
-
# @
|
|
253
|
-
|
|
254
|
-
|
|
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)
|
|
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
|