multi_compress 0.3.5 → 0.6.0

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -0
  3. data/GET_STARTED.md +207 -0
  4. data/README.md +69 -0
  5. data/db_core/include/mcdb_format.h +113 -0
  6. data/db_core/src/mcdb_format.c +594 -0
  7. data/db_deployment/mysql/Makefile +48 -0
  8. data/db_deployment/mysql/README.md +61 -0
  9. data/db_deployment/mysql/bin/common +212 -0
  10. data/db_deployment/mysql/bin/disable +40 -0
  11. data/db_deployment/mysql/bin/doctor +64 -0
  12. data/db_deployment/mysql/bin/enable +61 -0
  13. data/db_deployment/mysql/bin/install +64 -0
  14. data/db_deployment/mysql/bin/status +67 -0
  15. data/db_deployment/mysql/bin/uninstall +47 -0
  16. data/db_deployment/mysql/bin/upgrade +127 -0
  17. data/db_deployment/mysql/bin/verify +14 -0
  18. data/db_deployment/postgres/Makefile +45 -0
  19. data/db_deployment/postgres/README.md +84 -0
  20. data/db_deployment/postgres/bin/doctor +61 -0
  21. data/db_deployment/postgres/bin/enable +138 -0
  22. data/db_deployment/postgres/bin/install +106 -0
  23. data/db_deployment/postgres/bin/uninstall +54 -0
  24. data/db_deployment/postgres/bin/verify +14 -0
  25. data/docs/database-envelope-v1.md +159 -0
  26. data/docs/database-envelope-v2.md +147 -0
  27. data/docs/rfcs/0002-mcdb2-dictionaries.md +75 -0
  28. data/exe/multi_compress +228 -0
  29. data/ext/multi_compress/extconf.rb +4 -0
  30. data/ext/multi_compress/multi_compress.c +62 -0
  31. data/lib/multi_compress/active_record.rb +61 -0
  32. data/lib/multi_compress/codec.rb +164 -0
  33. data/lib/multi_compress/database.rb +312 -0
  34. data/lib/multi_compress/db_deployment.rb +868 -0
  35. data/lib/multi_compress/version.rb +1 -1
  36. data/multi_compress.gemspec +55 -0
  37. data/mysql_udf/Makefile +96 -0
  38. data/mysql_udf/README.md +140 -0
  39. data/mysql_udf/sql/examples.sql +16 -0
  40. data/mysql_udf/sql/install.sql +39 -0
  41. data/mysql_udf/sql/uninstall.sql +10 -0
  42. data/mysql_udf/sql/views.sql +20 -0
  43. data/mysql_udf/src/multi_compress_mysql.c +472 -0
  44. data/mysql_udf/src/mysql_udf_abi_57.h +34 -0
  45. data/mysql_udf/test/mcdb_cli.c +60 -0
  46. data/mysql_udf/test/run_e2e.sh +414 -0
  47. data/postgres_extension/Makefile +74 -0
  48. data/postgres_extension/README.md +160 -0
  49. data/postgres_extension/multi_compress.control +4 -0
  50. data/postgres_extension/multi_compress_pg.exports +24 -0
  51. data/postgres_extension/sql/examples.sql +23 -0
  52. data/postgres_extension/sql/multi_compress--0.5.0--0.6.0.sql +59 -0
  53. data/postgres_extension/sql/multi_compress--0.5.0.sql +24 -0
  54. data/postgres_extension/sql/multi_compress--0.6.0.sql +85 -0
  55. data/postgres_extension/sql/uninstall.sql +2 -0
  56. data/postgres_extension/sql/views.sql +7 -0
  57. data/postgres_extension/src/mcdb_format.c +2 -0
  58. data/postgres_extension/src/multi_compress_pg.c +381 -0
  59. data/postgres_extension/test/run_e2e.sh +411 -0
  60. metadata +56 -3
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "multi_compress"
4
+
5
+ module MultiCompress
6
+ class Codec
7
+ MAGIC = "\x89MCDB".b # reserved 5-byte namespace; 0x89 is an
8
+ # invalid UTF-8 lead byte, so it won't
9
+ # collide with valid text. For BINARY data
10
+ # this prefix is reserved, not collision-free.
11
+ VERSION = 1
12
+ HEADER_SIZE = MAGIC.bytesize + 2 # magic + version byte + algo byte
13
+ B64_PREFIX = "mc1:"
14
+ MAX_NATIVE_OUTPUT = (2**(0.size * 8 - 1)) - 1
15
+
16
+ ALGO_ID = { zstd: 1, lz4: 2, brotli: 3 }.freeze
17
+ ID_ALGO = ALGO_ID.invert.freeze
18
+
19
+ attr_reader :algo, :level, :max_output_size
20
+
21
+ def initialize(algo: :zstd, level: nil, encode: nil, serializer: nil,
22
+ encoding: Encoding::UTF_8, max_output_size: nil,
23
+ legacy: :reject, dictionary: nil)
24
+ raise ArgumentError, "unknown algo #{algo.inspect}" unless ALGO_ID.key?(algo)
25
+ raise ArgumentError, "encode must be nil or :base64" unless [nil, :base64].include?(encode)
26
+
27
+ @algo = algo
28
+ @level = level
29
+ @encode = encode
30
+ @serializer = serializer
31
+ @encoding = encoding
32
+ limit = Integer(max_output_size || MultiCompress.config.max_output_size)
33
+ raise ArgumentError, "max_output_size must be greater than 0" unless limit.positive?
34
+ if limit > MAX_NATIVE_OUTPUT
35
+ raise ArgumentError,
36
+ "max_output_size must be at most #{MAX_NATIVE_OUTPUT} bytes on this platform"
37
+ end
38
+
39
+ @max_output_size = limit
40
+ @legacy = normalize_legacy(legacy)
41
+ @dictionary = dictionary
42
+ end
43
+
44
+ def dump(value)
45
+ return nil if value.nil?
46
+
47
+ payload = serialize_payload(value)
48
+ body = MultiCompress.compress(payload, algo: @algo, level: @level, dictionary: @dictionary)
49
+ framed = header + body
50
+
51
+ @encode == :base64 ? B64_PREFIX + [framed].pack("m0") : framed
52
+ end
53
+
54
+ def load(stored)
55
+ return nil if stored.nil?
56
+
57
+ framed = decode_transport(stored)
58
+ return decode_payload(unwrap(framed)) if framed
59
+
60
+ read_legacy(stored)
61
+ end
62
+
63
+ private
64
+
65
+ def header
66
+ (MAGIC + [VERSION, ALGO_ID.fetch(@algo)].pack("C2")).b
67
+ end
68
+
69
+ def decode_transport(stored)
70
+ if @encode == :base64
71
+ s = stored.to_s
72
+ return nil unless s.start_with?(B64_PREFIX)
73
+
74
+ decoded = optional_base64(s.byteslice(B64_PREFIX.bytesize, s.bytesize - B64_PREFIX.bytesize))
75
+ decoded && decoded.start_with?(MAGIC) ? decoded : nil
76
+ else
77
+ b = stored.b
78
+ b.start_with?(MAGIC) ? b : nil
79
+ end
80
+ end
81
+
82
+ def unwrap(framed)
83
+ if framed.bytesize < HEADER_SIZE
84
+ raise MultiCompress::DataError, "MultiCompress::Codec: truncated envelope"
85
+ end
86
+
87
+ version = framed.getbyte(MAGIC.bytesize)
88
+ algo_id = framed.getbyte(MAGIC.bytesize + 1)
89
+ unless version == VERSION
90
+ raise MultiCompress::DataError, "MultiCompress::Codec: unsupported envelope version #{version}"
91
+ end
92
+ algo = ID_ALGO[algo_id]
93
+ raise MultiCompress::DataError, "MultiCompress::Codec: unknown algorithm id #{algo_id}" unless algo
94
+
95
+ body = framed.byteslice(HEADER_SIZE, framed.bytesize - HEADER_SIZE) || +"".b
96
+ MultiCompress.decompress(body, algo: algo, dictionary: @dictionary, max_output_size: @max_output_size)
97
+ end
98
+
99
+ def read_legacy(stored)
100
+ case @legacy[:mode]
101
+ when :reject
102
+ raise MultiCompress::DataError,
103
+ "MultiCompress::Codec: value has no envelope and legacy: :reject is set"
104
+ when :plain
105
+ decode_payload(stored.b.dup)
106
+ when :compressed
107
+ payload = MultiCompress.decompress(
108
+ stored.b, algo: @legacy[:algo], dictionary: @dictionary, max_output_size: @max_output_size
109
+ )
110
+ decode_payload(payload)
111
+ end
112
+ end
113
+
114
+ def normalize_legacy(legacy)
115
+ case legacy
116
+ when :reject, :plain
117
+ { mode: legacy }
118
+ when Hash
119
+ algo = legacy[:compressed]
120
+ raise ArgumentError, "legacy: { compressed: <algo> } required" unless ALGO_ID.key?(algo)
121
+
122
+ { mode: :compressed, algo: algo }
123
+ else
124
+ raise ArgumentError, "legacy: must be :reject, :plain, or { compressed: :algo }"
125
+ end
126
+ end
127
+
128
+ def serialize_payload(value)
129
+ if @serializer
130
+ @serializer.dump(value).b
131
+ else
132
+ unless value.is_a?(String)
133
+ raise TypeError, "MultiCompress::Codec expects a String (or pass serializer:); got #{value.class}"
134
+ end
135
+
136
+ unless @encoding == Encoding::BINARY || value.dup.force_encoding(@encoding).valid_encoding?
137
+ raise ArgumentError, "MultiCompress::Codec: value is not valid #{@encoding} " \
138
+ "(use encoding: Encoding::BINARY for arbitrary bytes)"
139
+ end
140
+ value.b
141
+ end
142
+ end
143
+
144
+ def decode_payload(bytes)
145
+ return @serializer.load(bytes) if @serializer
146
+
147
+ finalize_string(bytes)
148
+ end
149
+
150
+ def finalize_string(bytes)
151
+ s = bytes.dup.force_encoding(@encoding)
152
+ unless @encoding == Encoding::BINARY || s.valid_encoding?
153
+ raise MultiCompress::DataError, "MultiCompress::Codec: decoded bytes are not valid #{@encoding}"
154
+ end
155
+ s
156
+ end
157
+
158
+ def optional_base64(str)
159
+ str.unpack1("m0")&.b
160
+ rescue ArgumentError
161
+ nil
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "multi_compress"
5
+
6
+ module MultiCompress
7
+ # Long-term, SQL-readable envelopes. MCDB1 is dictionary-free. MCDB2 is an
8
+ # opt-in, dictionary-required format for one homogeneous database column.
9
+ module Database
10
+ MAGIC = "MCDB".b
11
+ VERSION_V1 = 1
12
+ VERSION_V2 = 2
13
+ VERSION = VERSION_V1 # MCDB1 compatibility alias
14
+ CODEC_ZSTD = 1
15
+ FLAGS_NONE = 0
16
+ FLAGS_V1 = FLAGS_NONE
17
+ V1_HEADER_SIZE = 19 # 4 magic + 1 version + 1 codec + 1 flags + 8 size + 4 crc32
18
+ V2_HEADER_SIZE = 27 # MCDB1 header + 8 byte application dictionary_ref
19
+ HEADER_SIZE = V1_HEADER_SIZE
20
+ ORIGINAL_SIZE_OFFSET = 7
21
+ CRC_OFFSET = 15
22
+ DICTIONARY_REF_OFFSET = 19
23
+ MAX_OUTPUT = 16 * 1024 * 1024
24
+ MAX_DICTIONARY_BYTES = 256 * 1024
25
+ MAX_DICTIONARY_REF = (2**63) - 1
26
+ MAX_ENVELOPE_V1 = 16_842_771
27
+ MAX_ENVELOPE_V2 = 16_842_779
28
+ MAX_ENVELOPE = MAX_ENVELOPE_V2
29
+ ZSTD_LEVEL = 6
30
+
31
+ # A strict, immutable MCDB2 dictionary. It is intentionally distinct from
32
+ # MultiCompress::Dictionary: MCDB2 accepts only conformant zstd dictionaries
33
+ # with a non-zero zstd DictID and an application-level registry id.
34
+ class Dictionary
35
+ attr_reader :id, :bytes, :sha256, :zstd_id, :native
36
+
37
+ def self.train(samples, id:, size: 32 * 1024)
38
+ new(id: id, native: MultiCompress::Zstd.train_dictionary(samples, size: size))
39
+ end
40
+
41
+ def self.wrap(native, id:)
42
+ new(id: id, native: native)
43
+ end
44
+
45
+ def initialize(id:, native: nil, bytes: nil)
46
+ @id = normalize_id(id)
47
+ raise ArgumentError, "provide exactly one of native: or bytes:" if (!!native) == (!!bytes)
48
+
49
+ @native = native || MultiCompress::Dictionary.new(bytes, algo: :zstd)
50
+ unless @native.is_a?(MultiCompress::Dictionary) && @native.algo == :zstd
51
+ raise TypeError, "MCDB2 dictionary must be a MultiCompress::Dictionary with algo: :zstd"
52
+ end
53
+
54
+ @bytes = @native.bytes.b.freeze
55
+ if @bytes.empty? || @bytes.bytesize > MAX_DICTIONARY_BYTES
56
+ raise ArgumentError,
57
+ "MCDB2 dictionary is #{@bytes.bytesize} bytes; allowed range is 1..#{MAX_DICTIONARY_BYTES}"
58
+ end
59
+
60
+ @zstd_id = @native.zstd_id
61
+ raise ArgumentError, "MCDB2 dictionary must be a conformant zstd dictionary with a non-zero DictID" if @zstd_id.zero?
62
+
63
+ @sha256 = Digest::SHA256.digest(@bytes).freeze
64
+ freeze
65
+ end
66
+
67
+ def sha256_hex
68
+ @sha256.unpack1("H*")
69
+ end
70
+
71
+ def registry_attributes
72
+ { id: @id, zstd_dict_id: @zstd_id, sha256: @sha256, bytes: @bytes }
73
+ end
74
+
75
+ private
76
+
77
+ def normalize_id(value)
78
+ raise TypeError, "MCDB2 dictionary id must be an Integer" unless value.respond_to?(:to_int)
79
+
80
+ integer = value.to_int
81
+ unless integer.between?(1, MAX_DICTIONARY_REF)
82
+ raise ArgumentError, "MCDB2 dictionary id must be in 1..#{MAX_DICTIONARY_REF}"
83
+ end
84
+ integer
85
+ end
86
+ end
87
+
88
+ module_function
89
+
90
+ # Without a dictionary this emits MCDB1. With Database::Dictionary it emits
91
+ # MCDB2. Choose one format per database column; mixed use is deliberately
92
+ # not what the generated dictionary view is optimized for.
93
+ def compress(text, dictionary: nil)
94
+ return compress_v1(text) if dictionary.nil?
95
+
96
+ compress_v2(text, dictionary: dictionary)
97
+ end
98
+
99
+ def compress_v1(text)
100
+ bytes = input_bytes!(text, format: "v1")
101
+ frame = MultiCompress.compress(bytes, algo: :zstd, level: ZSTD_LEVEL)
102
+ validate_canonical_zstd_frame!(frame, bytes.bytesize, expected_dictionary_id: 0)
103
+ envelope = header_v1(bytes.bytesize, MultiCompress.crc32(bytes)) << frame
104
+ check_envelope_size!(envelope, MAX_ENVELOPE_V1, "v1")
105
+ envelope
106
+ end
107
+
108
+ def compress_v2(text, dictionary:)
109
+ dictionary = database_dictionary!(dictionary)
110
+ bytes = input_bytes!(text, format: "v2")
111
+ frame = MultiCompress.compress(bytes, algo: :zstd, level: ZSTD_LEVEL, dictionary: dictionary.native)
112
+ validate_canonical_zstd_frame!(frame, bytes.bytesize, expected_dictionary_id: dictionary.zstd_id)
113
+ envelope = header_v2(bytes.bytesize, MultiCompress.crc32(bytes), dictionary.id) << frame
114
+ check_envelope_size!(envelope, MAX_ENVELOPE_V2, "v2")
115
+ envelope
116
+ end
117
+
118
+ def decompress(blob, dictionary: nil)
119
+ header = parse_header!(blob)
120
+ case header.fetch(:version)
121
+ when VERSION_V1
122
+ raise ArgumentError, "MCDB1 does not accept a dictionary" unless dictionary.nil?
123
+
124
+ decode_v1(blob.b, header)
125
+ when VERSION_V2
126
+ decode_v2(blob.b, header, dictionary)
127
+ else
128
+ err("unsupported version #{header.fetch(:version)}")
129
+ end
130
+ end
131
+
132
+ def valid?(blob, dictionary: nil)
133
+ decompress(blob, dictionary: dictionary)
134
+ true
135
+ rescue StandardError
136
+ false
137
+ end
138
+
139
+ # Parses only the envelope header. It is safe for registry/FK consistency
140
+ # checks and does not decompress the payload.
141
+ def dictionary_ref(blob)
142
+ header = parse_header!(blob)
143
+ return nil if header.fetch(:version) == VERSION_V1
144
+
145
+ header.fetch(:dictionary_ref)
146
+ end
147
+
148
+ def original_size(blob)
149
+ parse_header!(blob).fetch(:original_size)
150
+ end
151
+
152
+ def version_string
153
+ "MCDB1 + MCDB2 (zstd #{MultiCompress.version(:zstd)})"
154
+ end
155
+
156
+ def input_bytes!(text, format:)
157
+ raise TypeError, "MultiCompress::Database.compress expects a String, got #{text.class}" unless text.is_a?(String)
158
+
159
+ bytes = utf8_bytes(text)
160
+ n = bytes.bytesize
161
+ if n > MAX_OUTPUT
162
+ raise ArgumentError, "MultiCompress::Database: input is #{n} bytes, over the #{MAX_OUTPUT}-byte #{format} limit"
163
+ end
164
+ bytes
165
+ end
166
+ private_class_method :input_bytes!
167
+
168
+ def decode_v1(blob, header)
169
+ frame = blob.byteslice(V1_HEADER_SIZE, blob.bytesize - V1_HEADER_SIZE) || +"".b
170
+ validate_canonical_zstd_frame!(frame, header.fetch(:original_size), expected_dictionary_id: 0)
171
+ out = MultiCompress.decompress(frame, algo: :zstd, max_output_size: MAX_OUTPUT)
172
+ validate_decoded!(blob, header, out)
173
+ end
174
+ private_class_method :decode_v1
175
+
176
+ def decode_v2(blob, header, dictionary)
177
+ err("MCDB2 requires a dictionary") if dictionary.nil?
178
+ dictionary = database_dictionary!(dictionary)
179
+ if dictionary.id != header.fetch(:dictionary_ref)
180
+ err("dictionary reference #{header.fetch(:dictionary_ref)} does not match supplied dictionary #{dictionary.id}")
181
+ end
182
+
183
+ frame = blob.byteslice(V2_HEADER_SIZE, blob.bytesize - V2_HEADER_SIZE) || +"".b
184
+ validate_canonical_zstd_frame!(frame, header.fetch(:original_size), expected_dictionary_id: dictionary.zstd_id)
185
+ out = MultiCompress.decompress(
186
+ frame,
187
+ algo: :zstd,
188
+ dictionary: dictionary.native,
189
+ max_output_size: MAX_OUTPUT
190
+ )
191
+ validate_decoded!(blob, header, out)
192
+ end
193
+ private_class_method :decode_v2
194
+
195
+ def parse_header!(blob)
196
+ raise TypeError, "MultiCompress::Database.decompress expects a String, got #{blob.class}" unless blob.is_a?(String)
197
+
198
+ b = blob.b
199
+ err("truncated envelope (#{b.bytesize} < #{V1_HEADER_SIZE} bytes)") if b.bytesize < V1_HEADER_SIZE
200
+ err("bad magic") unless b.byteslice(0, 4) == MAGIC
201
+
202
+ version = b.getbyte(4)
203
+ case version
204
+ when VERSION_V1
205
+ header_size = V1_HEADER_SIZE
206
+ max_envelope = MAX_ENVELOPE_V1
207
+ when VERSION_V2
208
+ header_size = V2_HEADER_SIZE
209
+ max_envelope = MAX_ENVELOPE_V2
210
+ else
211
+ err("unsupported version #{version}")
212
+ end
213
+
214
+ err("truncated #{format_name(version)} envelope (#{b.bytesize} < #{header_size} bytes)") if b.bytesize < header_size
215
+ err("stored #{format_name(version)} envelope is #{b.bytesize} bytes, over the #{max_envelope}-byte limit") if b.bytesize > max_envelope
216
+ err("unsupported codec #{b.getbyte(5)}") unless b.getbyte(5) == CODEC_ZSTD
217
+ err("reserved flags must be 0, got #{b.getbyte(6)}") unless b.getbyte(6) == FLAGS_NONE
218
+
219
+ original_size = b.byteslice(ORIGINAL_SIZE_OFFSET, 8).unpack1("Q<")
220
+ err("declared size #{original_size} over the #{MAX_OUTPUT}-byte limit") if original_size > MAX_OUTPUT
221
+
222
+ header = {
223
+ version: version,
224
+ header_size: header_size,
225
+ original_size: original_size,
226
+ expected_crc: b.byteslice(CRC_OFFSET, 4).unpack1("L<")
227
+ }
228
+ if version == VERSION_V2
229
+ dictionary_ref = b.byteslice(DICTIONARY_REF_OFFSET, 8).unpack1("Q<")
230
+ unless dictionary_ref.between?(1, MAX_DICTIONARY_REF)
231
+ err("invalid dictionary reference #{dictionary_ref}")
232
+ end
233
+ header[:dictionary_ref] = dictionary_ref
234
+ end
235
+ header
236
+ end
237
+ private_class_method :parse_header!
238
+
239
+ def validate_decoded!(blob, header, out)
240
+ if out.bytesize != header.fetch(:original_size)
241
+ err("size mismatch: header says #{header.fetch(:original_size)}, got #{out.bytesize}")
242
+ end
243
+ actual_crc = MultiCompress.crc32(out)
244
+ if actual_crc != header.fetch(:expected_crc)
245
+ err("crc32 mismatch: header #{header.fetch(:expected_crc)}, computed #{actual_crc}")
246
+ end
247
+ s = out.force_encoding(Encoding::UTF_8)
248
+ err("decompressed bytes are not valid UTF-8") unless s.valid_encoding?
249
+ err("decompressed text contains a NUL byte") if s.include?("\0")
250
+ s
251
+ end
252
+ private_class_method :validate_decoded!
253
+
254
+ def utf8_bytes(text)
255
+ s = text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding(Encoding::UTF_8)
256
+ raise ArgumentError, "MultiCompress::Database: input is not valid UTF-8" unless s.valid_encoding?
257
+ raise ArgumentError, "MultiCompress::Database: input contains a NUL byte" if s.include?("\0")
258
+
259
+ s.b
260
+ end
261
+ private_class_method :utf8_bytes
262
+
263
+ def validate_canonical_zstd_frame!(frame, expected_size, expected_dictionary_id:)
264
+ _frame_size, content_size = MultiCompress.zstd_single_frame_info(frame)
265
+ err("zstd frame content size #{content_size} does not match header size #{expected_size}") if content_size != expected_size
266
+ actual_dictionary_id = MultiCompress.zstd_frame_dictionary_id(frame)
267
+ if actual_dictionary_id != expected_dictionary_id
268
+ err("zstd frame dictionary id #{actual_dictionary_id} does not match expected #{expected_dictionary_id}")
269
+ end
270
+ rescue MultiCompress::DataError => e
271
+ err(e.message.sub(/\AMultiCompress::Database: /, ""))
272
+ end
273
+ private_class_method :validate_canonical_zstd_frame!
274
+
275
+ def database_dictionary!(value)
276
+ return value if value.is_a?(Dictionary)
277
+
278
+ raise TypeError, "MCDB2 requires MultiCompress::Database::Dictionary, got #{value.class}"
279
+ end
280
+ private_class_method :database_dictionary!
281
+
282
+ def check_envelope_size!(envelope, max, format)
283
+ return if envelope.bytesize <= max
284
+
285
+ raise MultiCompress::DataError,
286
+ "MultiCompress::Database: stored #{format} envelope is #{envelope.bytesize} bytes, over the #{max}-byte limit"
287
+ end
288
+ private_class_method :check_envelope_size!
289
+
290
+ def header_v1(original_size, crc32)
291
+ (MAGIC + [VERSION_V1, CODEC_ZSTD, FLAGS_NONE].pack("C3") +
292
+ [original_size].pack("Q<") + [crc32].pack("L<")).b
293
+ end
294
+ private_class_method :header_v1
295
+
296
+ def header_v2(original_size, crc32, dictionary_ref)
297
+ (MAGIC + [VERSION_V2, CODEC_ZSTD, FLAGS_NONE].pack("C3") +
298
+ [original_size].pack("Q<") + [crc32].pack("L<") + [dictionary_ref].pack("Q<")).b
299
+ end
300
+ private_class_method :header_v2
301
+
302
+ def format_name(version)
303
+ version == VERSION_V2 ? "MCDB2" : "MCDB1"
304
+ end
305
+ private_class_method :format_name
306
+
307
+ def err(message)
308
+ raise MultiCompress::DataError, "MultiCompress::Database: #{message}"
309
+ end
310
+ private_class_method :err
311
+ end
312
+ end