omnizip 0.3.21 → 0.3.23
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/CHANGELOG.md +40 -0
- data/lib/omnizip/algorithms/zstandard/decoder.rb +62 -3
- data/lib/omnizip/algorithms/zstandard/dictionary.rb +73 -0
- data/lib/omnizip/algorithms/zstandard/encoder.rb +53 -12
- data/lib/omnizip/algorithms/zstandard/match_finder.rb +37 -8
- data/lib/omnizip/algorithms/zstandard.rb +51 -0
- data/lib/omnizip/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61cf26f9e324600f286b236ab80e02d720059f32bca2fa3c71640ba3abc4355a
|
|
4
|
+
data.tar.gz: fa189e081e402ecb4d084490e7874402faacf231a296cd009a15f218fcd9453e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 308660d6095a909d72c528bdfb5e828e76d320172b979a53ef7401aa2c1c12a560a0afd57f49991eb5190f87deca1c15cd798e8abae89bd3341ed3bb43c39b3d
|
|
7
|
+
data.tar.gz: 29ac9066539f2d8cef3973a2eb9cfe15a549a5efead04a1f5c88f28587fb6a2d23ac28c401c1c4187f5c226bfe4b531e3565523400dcd183adc83c842cd2d5dc
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.23] - 2026-08-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Zstd encoder hot paths use allocation-free integer reads instead of
|
|
14
|
+
`String#byteslice` + `unpack1` comparisons (profiling showed GC at
|
|
15
|
+
52% of encoder time): default-level compression is ~3.4x faster and
|
|
16
|
+
level 22 ~1.9x on the benchmark corpus, with byte-identical output.
|
|
17
|
+
|
|
18
|
+
## [0.3.22] - 2026-08-25
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Zstandard dictionary compression (`Dictionary.from_raw` /
|
|
22
|
+
`serialize` / `deserialize`, `compress_with_dict`,
|
|
23
|
+
`decompress_with_dict`): the dictionary content primes the match
|
|
24
|
+
finder as shared history and the frame header carries the
|
|
25
|
+
Dictionary_ID, which the decoder verifies (Phase-1 scope, as in the
|
|
26
|
+
Rust reference; entropy-table preloading is future work).
|
|
27
|
+
|
|
28
|
+
## [0.3.21] - 2026-08-25
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- 7-Zip encoder dictionary is now capped at the size announced in the
|
|
32
|
+
coder properties (prevents matches reading outside the decoder
|
|
33
|
+
window for inputs above the announced size).
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- Zstd lazy levels (6+) gained the rep0 fast-path and backward
|
|
37
|
+
extension: levels 6-12 improve 0.180 -> 0.154 and levels 19-22
|
|
38
|
+
0.146 -> 0.126 on the benchmark corpus; the level scale is now
|
|
39
|
+
monotonic.
|
|
40
|
+
- Zstd adaptive block splitting: heterogeneous chunks of 32 KiB or
|
|
41
|
+
more split into 16 KiB sub-blocks so entropy tables fit each
|
|
42
|
+
content regime.
|
|
43
|
+
|
|
44
|
+
## [0.3.20] - 2026-08-25
|
|
45
|
+
|
|
46
|
+
### Removed
|
|
47
|
+
- Dead `SevenZipLZMA2`/`XZLZMA2` algorithm wrappers (never
|
|
48
|
+
autoloaded; one referenced a nonexistent decoder constant).
|
|
49
|
+
|
|
10
50
|
## [0.3.19] - 2026-08-25
|
|
11
51
|
|
|
12
52
|
### Fixed
|
|
@@ -79,6 +79,35 @@ module Omnizip
|
|
|
79
79
|
output
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# Decode a stream produced by a dictionary-primed encoder: the
|
|
83
|
+
# dictionary's content primes the reconstruction window of
|
|
84
|
+
# frames carrying its ID (verified; mismatch raises).
|
|
85
|
+
#
|
|
86
|
+
# @param dict [Dictionary]
|
|
87
|
+
# @return [String] decompressed data (binary)
|
|
88
|
+
def decode_stream_with_dict(dict)
|
|
89
|
+
data = read_all
|
|
90
|
+
output = String.new(encoding: Encoding::BINARY)
|
|
91
|
+
pos = 0
|
|
92
|
+
|
|
93
|
+
loop do
|
|
94
|
+
remaining = data.bytesize - pos
|
|
95
|
+
break if remaining.zero?
|
|
96
|
+
raise Omnizip::DecompressionError, "trailing bytes are not a frame" if remaining < 4
|
|
97
|
+
|
|
98
|
+
magic = data.byteslice(pos, 4).unpack1("V")
|
|
99
|
+
unless magic == MAGIC_NUMBER
|
|
100
|
+
raise Omnizip::DecompressionError,
|
|
101
|
+
"invalid Zstandard magic: 0x#{magic.to_s(16)}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
frame_output, pos = decode_frame(data, pos + 4, dict)
|
|
105
|
+
output << frame_output
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
output
|
|
109
|
+
end
|
|
110
|
+
|
|
82
111
|
private
|
|
83
112
|
|
|
84
113
|
def read_all
|
|
@@ -101,8 +130,15 @@ module Omnizip
|
|
|
101
130
|
|
|
102
131
|
# Decode one frame (input positioned after the magic).
|
|
103
132
|
# Returns [frame_output, pos_after_frame].
|
|
104
|
-
|
|
133
|
+
#
|
|
134
|
+
# With a dictionary prefix, the reconstruction window is
|
|
135
|
+
# primed with the dictionary content so sequences can
|
|
136
|
+
# back-reference it; the prefix is verified (checksum) and
|
|
137
|
+
# stripped from the returned output.
|
|
138
|
+
def decode_frame(data, pos, dict = nil)
|
|
105
139
|
header, pos = Frame::Header.parse_from(data, pos)
|
|
140
|
+
verify_dictionary_id(header, dict)
|
|
141
|
+
prefix = dict&.content
|
|
106
142
|
|
|
107
143
|
# Reset per-frame state: repeat offsets, previous Huffman
|
|
108
144
|
# table, previous FSE tables.
|
|
@@ -111,6 +147,7 @@ module Omnizip
|
|
|
111
147
|
@previous_fse_tables = {}
|
|
112
148
|
|
|
113
149
|
output = String.new(encoding: Encoding::BINARY)
|
|
150
|
+
output << prefix if prefix
|
|
114
151
|
loop do
|
|
115
152
|
block, pos = Frame::Block.parse_from(data, pos)
|
|
116
153
|
if block.reserved?
|
|
@@ -128,7 +165,9 @@ module Omnizip
|
|
|
128
165
|
end
|
|
129
166
|
|
|
130
167
|
expected = data.byteslice(pos, 4).unpack1("V")
|
|
131
|
-
actual = XXHash64.frame_checksum(
|
|
168
|
+
actual = XXHash64.frame_checksum(
|
|
169
|
+
prefix ? output.byteslice(prefix.bytesize..) : output,
|
|
170
|
+
)
|
|
132
171
|
if expected != actual
|
|
133
172
|
raise Omnizip::DecompressionError,
|
|
134
173
|
"frame checksum mismatch: stored 0x#{expected.to_s(16)}, " \
|
|
@@ -137,9 +176,29 @@ module Omnizip
|
|
|
137
176
|
pos += 4
|
|
138
177
|
end
|
|
139
178
|
|
|
140
|
-
|
|
179
|
+
frame_output = prefix ? output.byteslice(prefix.bytesize..) : output
|
|
180
|
+
[frame_output, pos]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A frame that carries a Dictionary_ID must be decoded with a
|
|
184
|
+
# dictionary of the same ID; a mismatched or missing
|
|
185
|
+
# dictionary would silently garble every back-reference.
|
|
186
|
+
def verify_dictionary_id(header, dict)
|
|
187
|
+
return unless header.dictionary_id?
|
|
188
|
+
|
|
189
|
+
frame_id = header.dictionary_id
|
|
190
|
+
if dict.nil?
|
|
191
|
+
raise Omnizip::DecompressionError,
|
|
192
|
+
"frame requires dictionary #{frame_id}, none supplied"
|
|
193
|
+
end
|
|
194
|
+
return if dict.id == frame_id
|
|
195
|
+
|
|
196
|
+
raise Omnizip::DecompressionError,
|
|
197
|
+
"frame requires dictionary #{frame_id}, got #{dict.id}"
|
|
141
198
|
end
|
|
142
199
|
|
|
200
|
+
# Decode one block into `output`. Returns the position after
|
|
201
|
+
# the block's payload.
|
|
143
202
|
def decode_block(block, data, pos, output)
|
|
144
203
|
case block.block_type
|
|
145
204
|
when BLOCK_TYPE_RAW
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
# Zstandard dictionary (port of the omnizip-rs dict.rs).
|
|
27
|
+
#
|
|
28
|
+
# A dictionary lets the encoder preload a reference-content
|
|
29
|
+
# window so small inputs compress dramatically better: the
|
|
30
|
+
# dictionary content is used as a match-finder prefix, and the
|
|
31
|
+
# frame header carries the dictionary's ID.
|
|
32
|
+
#
|
|
33
|
+
# Wire format (simplified form, as in the Rust reference):
|
|
34
|
+
# magic (4) + dictionary ID (4, LE) + raw content.
|
|
35
|
+
class Dictionary
|
|
36
|
+
DICT_MAGIC = 0xEC30A437
|
|
37
|
+
|
|
38
|
+
attr_reader :id, :content
|
|
39
|
+
|
|
40
|
+
# @param id [Integer] dictionary ID carried in frame headers
|
|
41
|
+
# @param content [String] corpus bytes used as the prefix
|
|
42
|
+
def initialize(id, content)
|
|
43
|
+
@id = id
|
|
44
|
+
@content = content.dup.force_encoding(Encoding::BINARY)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.from_raw(id, content)
|
|
48
|
+
new(id, content)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.deserialize(data)
|
|
52
|
+
if data.bytesize < 8
|
|
53
|
+
raise Omnizip::DecompressionError,
|
|
54
|
+
"dictionary too short for magic + id"
|
|
55
|
+
end
|
|
56
|
+
if data.byteslice(0, 4).unpack1("V") != DICT_MAGIC
|
|
57
|
+
raise Omnizip::DecompressionError, "bad dictionary magic"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
new(data.byteslice(4, 4).unpack1("V"), data.byteslice(8..))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def serialize
|
|
64
|
+
[DICT_MAGIC].pack("V") + [@id].pack("V") + @content
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ==(other)
|
|
68
|
+
other.is_a?(Dictionary) && id == other.id && content == other.content
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -58,42 +58,83 @@ module Omnizip
|
|
|
58
58
|
write_frame(data)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Encode a data stream primed with a dictionary prefix (port
|
|
62
|
+
# of the Rust encode_frame_with_dict): the dictionary content
|
|
63
|
+
# is prepended as virtual history, the match finder is seeded
|
|
64
|
+
# with its positions, and only the plaintext is emitted.
|
|
65
|
+
# Frame_Content_Size and the checksum cover the plaintext
|
|
66
|
+
# only; the frame header carries the dictionary's ID and
|
|
67
|
+
# requires the dict-aware decoder.
|
|
68
|
+
#
|
|
69
|
+
# @param data [String]
|
|
70
|
+
# @param dict [Dictionary]
|
|
71
|
+
def encode_stream_with_dict(data, dict)
|
|
72
|
+
data = data.dup.force_encoding(Encoding::BINARY)
|
|
73
|
+
virtual = dict.content + data
|
|
74
|
+
prefix_len = dict.content.bytesize
|
|
75
|
+
|
|
76
|
+
write_u32le(MAGIC_NUMBER)
|
|
77
|
+
write_frame_header(data, dict.id)
|
|
78
|
+
write_blocks(virtual, prefix_len)
|
|
79
|
+
write_u32le(XXHash64.frame_checksum(data)) if @checksum
|
|
80
|
+
end
|
|
81
|
+
|
|
61
82
|
private
|
|
62
83
|
|
|
63
|
-
def write_frame(data)
|
|
84
|
+
def write_frame(data, dict = nil)
|
|
64
85
|
write_u32le(MAGIC_NUMBER)
|
|
65
|
-
write_frame_header(data)
|
|
86
|
+
write_frame_header(data, dict && dict.id)
|
|
66
87
|
write_blocks(data)
|
|
67
88
|
write_u32le(XXHash64.frame_checksum(data)) if @checksum
|
|
68
89
|
end
|
|
69
90
|
|
|
70
|
-
# Single-segment frame header: no window descriptor
|
|
71
|
-
#
|
|
72
|
-
|
|
91
|
+
# Single-segment frame header: no window descriptor;
|
|
92
|
+
# optional Dictionary_ID, then Frame_Content_Size in 1, 2, 4
|
|
93
|
+
# or 8 bytes.
|
|
94
|
+
def write_frame_header(data, dict_id = nil)
|
|
95
|
+
did_flag, did_bytes = dict_id_encoding(dict_id)
|
|
96
|
+
|
|
73
97
|
size = data.bytesize
|
|
74
98
|
if size < 256
|
|
75
|
-
@output_stream.putc(0x20) # single segment, FCS 1 byte
|
|
99
|
+
@output_stream.putc(0x20 | did_flag) # single segment, FCS 1 byte
|
|
100
|
+
write_dict_id_bytes(did_bytes)
|
|
76
101
|
@output_stream.putc(size)
|
|
77
102
|
elsif size <= 65_791
|
|
78
|
-
@output_stream.putc(0x60) # single segment, FCS 2 bytes
|
|
103
|
+
@output_stream.putc(0x60 | did_flag) # single segment, FCS 2 bytes
|
|
104
|
+
write_dict_id_bytes(did_bytes)
|
|
79
105
|
write_u16le(size - 256)
|
|
80
106
|
elsif size <= 0xFFFFFFFF
|
|
81
|
-
@output_stream.putc(0xA0) # single segment, FCS 4 bytes
|
|
107
|
+
@output_stream.putc(0xA0 | did_flag) # single segment, FCS 4 bytes
|
|
108
|
+
write_dict_id_bytes(did_bytes)
|
|
82
109
|
write_u32le(size)
|
|
83
110
|
else
|
|
84
|
-
@output_stream.putc(0xE0) # single segment, FCS 8 bytes
|
|
111
|
+
@output_stream.putc(0xE0 | did_flag) # single segment, FCS 8 bytes
|
|
112
|
+
write_dict_id_bytes(did_bytes)
|
|
85
113
|
write_u64le(size)
|
|
86
114
|
end
|
|
87
115
|
end
|
|
88
116
|
|
|
117
|
+
def dict_id_encoding(dict_id)
|
|
118
|
+
return [0, "".b] if dict_id.nil? || dict_id.zero?
|
|
119
|
+
return [1, [dict_id].pack("C")] if dict_id <= 0xFF
|
|
120
|
+
return [2, [dict_id].pack("v")] if dict_id <= 0xFFFF
|
|
121
|
+
|
|
122
|
+
[3, [dict_id].pack("V")]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def write_dict_id_bytes(bytes)
|
|
126
|
+
@output_stream.write(bytes)
|
|
127
|
+
end
|
|
128
|
+
|
|
89
129
|
# rubocop:disable Metrics/MethodLength
|
|
90
130
|
# rubocop:disable-next Metrics/AbcSize
|
|
91
|
-
def write_blocks(data)
|
|
92
|
-
return write_empty_last_block if data.
|
|
131
|
+
def write_blocks(data, start = 0)
|
|
132
|
+
return write_empty_last_block if data.bytesize <= start
|
|
93
133
|
|
|
94
134
|
params = MatchFinder.params_for_level(@level, data.bytesize)
|
|
95
135
|
ms = MatchFinder::MatchState.new(params[:hash_log])
|
|
96
136
|
ms.enable_chain(params[:chain]) if params[:chain].positive?
|
|
137
|
+
ms.seed_prefix(data, start) if start.positive?
|
|
97
138
|
|
|
98
139
|
# LDM (long-distance matching) at high levels on multi-block
|
|
99
140
|
# inputs: a sparse table over the whole frame finds matches
|
|
@@ -115,7 +156,7 @@ module Omnizip
|
|
|
115
156
|
# untouched.
|
|
116
157
|
reps = [1, 4, 8]
|
|
117
158
|
|
|
118
|
-
offset =
|
|
159
|
+
offset = start
|
|
119
160
|
while offset < data.bytesize
|
|
120
161
|
block_end = [offset + BLOCK_MAX_SIZE, data.bytesize].min
|
|
121
162
|
is_last = block_end == data.bytesize
|
|
@@ -88,6 +88,18 @@ module Omnizip
|
|
|
88
88
|
def disable_chain
|
|
89
89
|
@max_chain = 0
|
|
90
90
|
end
|
|
91
|
+
|
|
92
|
+
# Seed the hash table with a prefix's positions (dictionary
|
|
93
|
+
# content prepended to the plaintext): the most recent
|
|
94
|
+
# position per hash wins, matching insert order.
|
|
95
|
+
def seed_prefix(src, prefix_len)
|
|
96
|
+
return if prefix_len < MIN_MATCH
|
|
97
|
+
|
|
98
|
+
limit = prefix_len - MIN_MATCH + 1
|
|
99
|
+
(0...limit).each do |pos|
|
|
100
|
+
@hash_table[MatchFinder.hash4(src, pos, @hash_log)] = pos
|
|
101
|
+
end
|
|
102
|
+
end
|
|
91
103
|
end
|
|
92
104
|
|
|
93
105
|
module_function
|
|
@@ -119,11 +131,29 @@ module Omnizip
|
|
|
119
131
|
min_match: MIN_MATCH_ECONOMICAL }
|
|
120
132
|
end
|
|
121
133
|
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
# Allocation-free little-endian reads; the hot loops call
|
|
135
|
+
# these millions of times, and String#byteslice + unpack1
|
|
136
|
+
# showed up as half the encoder's time via GC pressure.
|
|
137
|
+
def read4(src, pos)
|
|
138
|
+
src.getbyte(pos) |
|
|
124
139
|
(src.getbyte(pos + 1) << 8) |
|
|
125
140
|
(src.getbyte(pos + 2) << 16) |
|
|
126
141
|
(src.getbyte(pos + 3) << 24)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def read8(src, pos)
|
|
145
|
+
src.getbyte(pos) |
|
|
146
|
+
(src.getbyte(pos + 1) << 8) |
|
|
147
|
+
(src.getbyte(pos + 2) << 16) |
|
|
148
|
+
(src.getbyte(pos + 3) << 24) |
|
|
149
|
+
(src.getbyte(pos + 4) << 32) |
|
|
150
|
+
(src.getbyte(pos + 5) << 40) |
|
|
151
|
+
(src.getbyte(pos + 6) << 48) |
|
|
152
|
+
(src.getbyte(pos + 7) << 56)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def hash4(src, pos, h_bits)
|
|
156
|
+
v = read4(src, pos)
|
|
127
157
|
# 32-bit wrapping multiply (C/uint32_t), then the high bits.
|
|
128
158
|
((v * PRIME4_BYTES) & 0xFFFFFFFF) >> (32 - h_bits)
|
|
129
159
|
end
|
|
@@ -136,8 +166,8 @@ module Omnizip
|
|
|
136
166
|
b_size = b.bytesize
|
|
137
167
|
while len + 8 <= limit && a_pos + len + 8 <= a_size &&
|
|
138
168
|
b_pos + len + 8 <= b_size
|
|
139
|
-
wa = a
|
|
140
|
-
wb = b
|
|
169
|
+
wa = read8(a, a_pos + len)
|
|
170
|
+
wb = read8(b, b_pos + len)
|
|
141
171
|
if wa == wb
|
|
142
172
|
len += 8
|
|
143
173
|
else
|
|
@@ -277,8 +307,7 @@ module Omnizip
|
|
|
277
307
|
# rubocop:disable-next Metrics/AbcSize
|
|
278
308
|
def rep0_match(src, ip, rep0, min_match, limit, anchor)
|
|
279
309
|
return nil if rep0 <= 0 || ip <= rep0
|
|
280
|
-
return nil if src
|
|
281
|
-
src.byteslice(ip - rep0, MIN_MATCH)
|
|
310
|
+
return nil if read4(src, ip) != read4(src, ip - rep0)
|
|
282
311
|
|
|
283
312
|
m_len = MIN_MATCH + count_match(
|
|
284
313
|
src, ip + MIN_MATCH, src, ip + MIN_MATCH - rep0,
|
|
@@ -334,7 +363,7 @@ module Omnizip
|
|
|
334
363
|
break if dist >= max_distance
|
|
335
364
|
break if candidate + MIN_MATCH > size
|
|
336
365
|
|
|
337
|
-
if src
|
|
366
|
+
if read4(src, ip) == read4(src, candidate)
|
|
338
367
|
m_len = MIN_MATCH + count_match(src, ip + MIN_MATCH,
|
|
339
368
|
src, candidate + MIN_MATCH,
|
|
340
369
|
[max_extend - MIN_MATCH, 0].max)
|
|
@@ -385,7 +414,7 @@ module Omnizip
|
|
|
385
414
|
if candidate.positive? && candidate < ip
|
|
386
415
|
dist = ip - candidate
|
|
387
416
|
if dist < max_distance && candidate + MIN_MATCH <= size &&
|
|
388
|
-
src
|
|
417
|
+
read4(src, ip) == read4(src, candidate)
|
|
389
418
|
best_len = MIN_MATCH + count_match(
|
|
390
419
|
src, ip + MIN_MATCH, src, candidate + MIN_MATCH,
|
|
391
420
|
[limit + MIN_MATCH_ECONOMICAL - ip - MIN_MATCH, 0].max
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
3
5
|
# Copyright (C) 2025 Ribose Inc.
|
|
4
6
|
#
|
|
5
7
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
@@ -58,6 +60,7 @@ module Omnizip
|
|
|
58
60
|
"omnizip/algorithms/zstandard/sequences_encoder"
|
|
59
61
|
autoload :MatchFinder, "omnizip/algorithms/zstandard/match_finder"
|
|
60
62
|
autoload :LdmHashTable, "omnizip/algorithms/zstandard/ldm"
|
|
63
|
+
autoload :Dictionary, "omnizip/algorithms/zstandard/dictionary"
|
|
61
64
|
autoload :XXHash64, "omnizip/algorithms/zstandard/xxhash"
|
|
62
65
|
|
|
63
66
|
# Frame and FSE modules
|
|
@@ -101,6 +104,54 @@ module Omnizip
|
|
|
101
104
|
output_stream.write(decompressed)
|
|
102
105
|
end
|
|
103
106
|
|
|
107
|
+
# Compress data primed with a dictionary prefix: the dictionary
|
|
108
|
+
# content acts as shared history the match finder can reference,
|
|
109
|
+
# dramatically improving ratios on small inputs. The resulting
|
|
110
|
+
# frame carries the dictionary's ID and requires
|
|
111
|
+
# decompress_with_dict to decode.
|
|
112
|
+
#
|
|
113
|
+
# @param input_stream [IO] plaintext
|
|
114
|
+
# @param output_stream [IO] compressed frame
|
|
115
|
+
# @param dict [Dictionary]
|
|
116
|
+
# @param options [Hash] :level, :checksum
|
|
117
|
+
# @return [void]
|
|
118
|
+
def compress_with_dict(input_stream, output_stream, dict, options = {})
|
|
119
|
+
encoder = Encoder.new(output_stream, build_encoder_options(options))
|
|
120
|
+
encoder.encode_stream_with_dict(input_stream.read, dict)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Decompress a frame produced by compress_with_dict. The frame's
|
|
124
|
+
# dictionary ID is verified against `dict` before decoding.
|
|
125
|
+
#
|
|
126
|
+
# @param input_stream [IO] compressed frame
|
|
127
|
+
# @param output_stream [IO] plaintext
|
|
128
|
+
# @param dict [Dictionary]
|
|
129
|
+
# @return [void]
|
|
130
|
+
def decompress_with_dict(input_stream, output_stream, dict)
|
|
131
|
+
output_stream.set_encoding(Encoding::BINARY)
|
|
132
|
+
decoder = Decoder.new(input_stream)
|
|
133
|
+
output_stream.write(decoder.decode_stream_with_dict(dict))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Class-level convenience mirrors of the dictionary API.
|
|
137
|
+
#
|
|
138
|
+
# @return [String] compressed frame / decompressed plaintext
|
|
139
|
+
def self.compress_with_dict(data, dict, **options)
|
|
140
|
+
output = StringIO.new
|
|
141
|
+
output.set_encoding(Encoding::BINARY)
|
|
142
|
+
new(options).compress_with_dict(
|
|
143
|
+
StringIO.new(data.to_s.b), output, dict, options
|
|
144
|
+
)
|
|
145
|
+
output.string
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.decompress_with_dict(data, dict)
|
|
149
|
+
output = StringIO.new
|
|
150
|
+
output.set_encoding(Encoding::BINARY)
|
|
151
|
+
new.decompress_with_dict(StringIO.new(data.to_s.b), output, dict)
|
|
152
|
+
output.string
|
|
153
|
+
end
|
|
154
|
+
|
|
104
155
|
private
|
|
105
156
|
|
|
106
157
|
# Build encoder options from compression options
|
data/lib/omnizip/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.23
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -301,6 +301,7 @@ files:
|
|
|
301
301
|
- lib/omnizip/algorithms/zstandard.rb
|
|
302
302
|
- lib/omnizip/algorithms/zstandard/constants.rb
|
|
303
303
|
- lib/omnizip/algorithms/zstandard/decoder.rb
|
|
304
|
+
- lib/omnizip/algorithms/zstandard/dictionary.rb
|
|
304
305
|
- lib/omnizip/algorithms/zstandard/encoder.rb
|
|
305
306
|
- lib/omnizip/algorithms/zstandard/frame.rb
|
|
306
307
|
- lib/omnizip/algorithms/zstandard/frame/block.rb
|