multi_compress 0.5.0 → 0.6.1
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 +45 -0
- data/GET_STARTED.md +20 -1
- data/README.md +15 -2
- data/THIRD_PARTY_NOTICES.md +1 -1
- data/db_core/include/mcdb_format.h +67 -9
- data/db_core/src/mcdb_format.c +392 -139
- data/db_deployment/mysql/README.md +19 -0
- data/db_deployment/mysql/bin/common +70 -18
- data/db_deployment/mysql/bin/enable +5 -1
- data/db_deployment/mysql/bin/status +2 -0
- data/db_deployment/mysql/bin/upgrade +17 -5
- data/db_deployment/postgres/README.md +24 -0
- data/db_deployment/postgres/bin/enable +2 -0
- data/db_deployment/postgres/bin/install +4 -2
- data/docs/database-envelope-v2.md +147 -0
- data/docs/rfcs/0002-mcdb2-dictionaries.md +75 -0
- data/ext/multi_compress/multi_compress.c +261 -76
- data/ext/multi_compress/vendor/.vendored +1 -1
- data/ext/multi_compress/vendor/brotli/c/common/version.h +2 -2
- data/ext/multi_compress/vendor/brotli/c/dec/decode.c +59 -30
- data/ext/multi_compress/vendor/brotli/c/include/brotli/decode.h +1 -3
- data/lib/multi_compress/active_record.rb +11 -5
- data/lib/multi_compress/codec.rb +164 -94
- data/lib/multi_compress/database/api.rb +42 -0
- data/lib/multi_compress/database/decoding.rb +42 -0
- data/lib/multi_compress/database/dictionary.rb +72 -0
- data/lib/multi_compress/database/encoding.rb +31 -0
- data/lib/multi_compress/database/header.rb +71 -0
- data/lib/multi_compress/database/operations.rb +73 -0
- data/lib/multi_compress/database/validation.rb +95 -0
- data/lib/multi_compress/database.rb +29 -95
- data/lib/multi_compress/db_deployment/bundle_archiver.rb +70 -0
- data/lib/multi_compress/db_deployment/bundle_metadata.rb +56 -0
- data/lib/multi_compress/db_deployment/bundle_stager.rb +62 -0
- data/lib/multi_compress/db_deployment/commands.rb +309 -0
- data/lib/multi_compress/db_deployment/deployment_bundle.rb +80 -0
- data/lib/multi_compress/db_deployment/deterministic_tar_writer.rb +76 -0
- data/lib/multi_compress/db_deployment/dictionary_registry.rb +244 -0
- data/lib/multi_compress/db_deployment/readable_view.rb +112 -0
- data/lib/multi_compress/db_deployment/readable_view_options.rb +51 -0
- data/lib/multi_compress/db_deployment/sql_identifiers.rb +45 -0
- data/lib/multi_compress/db_deployment.rb +13 -435
- data/lib/multi_compress/reader/buffer_operations.rb +57 -0
- data/lib/multi_compress/reader/read_operations.rb +67 -0
- data/lib/multi_compress/reader.rb +97 -0
- data/lib/multi_compress/stream_io.rb +13 -0
- data/lib/multi_compress/version.rb +1 -1
- data/lib/multi_compress/writer.rb +85 -0
- data/lib/multi_compress.rb +42 -333
- data/mysql_udf/README.md +45 -0
- data/mysql_udf/sql/examples.sql +5 -0
- data/mysql_udf/sql/install.sql +25 -1
- data/mysql_udf/sql/uninstall.sql +7 -1
- data/mysql_udf/sql/views.sql +16 -4
- data/mysql_udf/src/multi_compress_mysql.c +383 -44
- data/mysql_udf/test/run_e2e.sh +119 -1
- data/postgres_extension/Makefile +2 -2
- data/postgres_extension/README.md +48 -0
- data/postgres_extension/multi_compress.control +2 -2
- data/postgres_extension/multi_compress_pg.exports +12 -0
- data/postgres_extension/sql/examples.sql +11 -0
- data/postgres_extension/sql/multi_compress--0.5.0--0.6.0.sql +59 -0
- data/postgres_extension/sql/multi_compress--0.6.0.sql +85 -0
- data/postgres_extension/src/multi_compress_pg.c +301 -16
- data/postgres_extension/test/run_e2e.sh +90 -2
- metadata +29 -6
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
#define BROTLI_VERSION_MAJOR 1
|
|
21
21
|
#define BROTLI_VERSION_MINOR 2
|
|
22
|
-
#define BROTLI_VERSION_PATCH
|
|
22
|
+
#define BROTLI_VERSION_PATCH 1
|
|
23
23
|
|
|
24
24
|
#define BROTLI_VERSION BROTLI_MAKE_HEX_VERSION( \
|
|
25
25
|
BROTLI_VERSION_MAJOR, BROTLI_VERSION_MINOR, BROTLI_VERSION_PATCH)
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
#define BROTLI_ABI_CURRENT 3
|
|
36
|
-
#define BROTLI_ABI_REVISION
|
|
36
|
+
#define BROTLI_ABI_REVISION 1
|
|
37
37
|
#define BROTLI_ABI_AGE 2
|
|
38
38
|
|
|
39
39
|
#if BROTLI_VERSION_MAJOR != (BROTLI_ABI_CURRENT - BROTLI_ABI_AGE)
|
|
@@ -1176,7 +1176,7 @@ static BrotliDecoderErrorCode DecodeContextMap(brotli_reg_t context_map_size,
|
|
|
1176
1176
|
|
|
1177
1177
|
/* Decodes a command or literal and updates block type ring-buffer.
|
|
1178
1178
|
Reads 3..54 bits. */
|
|
1179
|
-
static BROTLI_INLINE
|
|
1179
|
+
static BROTLI_INLINE BrotliDecoderErrorCode DecodeBlockTypeAndLength(
|
|
1180
1180
|
int safe, BrotliDecoderState* s, int tree_type) {
|
|
1181
1181
|
brotli_reg_t max_block_type = s->num_block_types[tree_type];
|
|
1182
1182
|
const HuffmanCode* type_tree = &s->block_type_trees[
|
|
@@ -1187,7 +1187,7 @@ static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
|
|
|
1187
1187
|
brotli_reg_t* ringbuffer = &s->block_type_rb[tree_type * 2];
|
|
1188
1188
|
brotli_reg_t block_type;
|
|
1189
1189
|
if (max_block_type <= 1) {
|
|
1190
|
-
return
|
|
1190
|
+
return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
|
|
1191
1191
|
}
|
|
1192
1192
|
|
|
1193
1193
|
/* Read 0..15 + 3..39 bits. */
|
|
@@ -1197,11 +1197,13 @@ static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
|
|
|
1197
1197
|
} else {
|
|
1198
1198
|
BrotliBitReaderState memento;
|
|
1199
1199
|
BrotliBitReaderSaveState(br, &memento);
|
|
1200
|
-
if (!SafeReadSymbol(type_tree, br, &block_type))
|
|
1200
|
+
if (!SafeReadSymbol(type_tree, br, &block_type)) {
|
|
1201
|
+
return BROTLI_DECODER_NEEDS_MORE_INPUT;
|
|
1202
|
+
}
|
|
1201
1203
|
if (!SafeReadBlockLength(s, &s->block_length[tree_type], len_tree, br)) {
|
|
1202
1204
|
s->substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_NONE;
|
|
1203
1205
|
BrotliBitReaderRestoreState(br, &memento);
|
|
1204
|
-
return
|
|
1206
|
+
return BROTLI_DECODER_NEEDS_MORE_INPUT;
|
|
1205
1207
|
}
|
|
1206
1208
|
}
|
|
1207
1209
|
|
|
@@ -1217,7 +1219,7 @@ static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
|
|
|
1217
1219
|
}
|
|
1218
1220
|
ringbuffer[0] = ringbuffer[1];
|
|
1219
1221
|
ringbuffer[1] = block_type;
|
|
1220
|
-
return
|
|
1222
|
+
return BROTLI_DECODER_SUCCESS;
|
|
1221
1223
|
}
|
|
1222
1224
|
|
|
1223
1225
|
static BROTLI_INLINE void DetectTrivialLiteralBlockTypes(
|
|
@@ -1254,62 +1256,68 @@ static BROTLI_INLINE void PrepareLiteralDecoding(BrotliDecoderState* s) {
|
|
|
1254
1256
|
|
|
1255
1257
|
/* Decodes the block type and updates the state for literal context.
|
|
1256
1258
|
Reads 3..54 bits. */
|
|
1257
|
-
static BROTLI_INLINE
|
|
1259
|
+
static BROTLI_INLINE BrotliDecoderErrorCode DecodeLiteralBlockSwitchInternal(
|
|
1258
1260
|
int safe, BrotliDecoderState* s) {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
+
BrotliDecoderErrorCode result = DecodeBlockTypeAndLength(safe, s, 0);
|
|
1262
|
+
if (result != BROTLI_DECODER_SUCCESS) {
|
|
1263
|
+
return result;
|
|
1261
1264
|
}
|
|
1262
1265
|
PrepareLiteralDecoding(s);
|
|
1263
|
-
return
|
|
1266
|
+
return BROTLI_DECODER_SUCCESS;
|
|
1264
1267
|
}
|
|
1265
1268
|
|
|
1266
|
-
static
|
|
1267
|
-
|
|
1269
|
+
static BROTLI_NOINLINE BrotliDecoderErrorCode
|
|
1270
|
+
DecodeLiteralBlockSwitch(BrotliDecoderState* s) {
|
|
1271
|
+
return DecodeLiteralBlockSwitchInternal(0, s);
|
|
1268
1272
|
}
|
|
1269
1273
|
|
|
1270
|
-
static
|
|
1274
|
+
static BROTLI_NOINLINE BrotliDecoderErrorCode SafeDecodeLiteralBlockSwitch(
|
|
1271
1275
|
BrotliDecoderState* s) {
|
|
1272
1276
|
return DecodeLiteralBlockSwitchInternal(1, s);
|
|
1273
1277
|
}
|
|
1274
1278
|
|
|
1275
1279
|
/* Block switch for insert/copy length.
|
|
1276
1280
|
Reads 3..54 bits. */
|
|
1277
|
-
static BROTLI_INLINE
|
|
1281
|
+
static BROTLI_INLINE BrotliDecoderErrorCode DecodeCommandBlockSwitchInternal(
|
|
1278
1282
|
int safe, BrotliDecoderState* s) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1283
|
+
BrotliDecoderErrorCode result = DecodeBlockTypeAndLength(safe, s, 1);
|
|
1284
|
+
if (result != BROTLI_DECODER_SUCCESS) {
|
|
1285
|
+
return result;
|
|
1281
1286
|
}
|
|
1282
1287
|
s->htree_command = s->insert_copy_hgroup.htrees[s->block_type_rb[3]];
|
|
1283
|
-
return
|
|
1288
|
+
return BROTLI_DECODER_SUCCESS;
|
|
1284
1289
|
}
|
|
1285
1290
|
|
|
1286
|
-
static
|
|
1287
|
-
|
|
1291
|
+
static BROTLI_NOINLINE BrotliDecoderErrorCode
|
|
1292
|
+
DecodeCommandBlockSwitch(BrotliDecoderState* s) {
|
|
1293
|
+
return DecodeCommandBlockSwitchInternal(0, s);
|
|
1288
1294
|
}
|
|
1289
1295
|
|
|
1290
|
-
static
|
|
1291
|
-
|
|
1296
|
+
static BROTLI_NOINLINE BrotliDecoderErrorCode
|
|
1297
|
+
SafeDecodeCommandBlockSwitch(BrotliDecoderState* s) {
|
|
1292
1298
|
return DecodeCommandBlockSwitchInternal(1, s);
|
|
1293
1299
|
}
|
|
1294
1300
|
|
|
1295
1301
|
/* Block switch for distance codes.
|
|
1296
1302
|
Reads 3..54 bits. */
|
|
1297
|
-
static BROTLI_INLINE
|
|
1303
|
+
static BROTLI_INLINE BrotliDecoderErrorCode DecodeDistanceBlockSwitchInternal(
|
|
1298
1304
|
int safe, BrotliDecoderState* s) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1305
|
+
BrotliDecoderErrorCode result = DecodeBlockTypeAndLength(safe, s, 2);
|
|
1306
|
+
if (result != BROTLI_DECODER_SUCCESS) {
|
|
1307
|
+
return result;
|
|
1301
1308
|
}
|
|
1302
1309
|
s->dist_context_map_slice = s->dist_context_map +
|
|
1303
1310
|
(s->block_type_rb[5] << BROTLI_DISTANCE_CONTEXT_BITS);
|
|
1304
1311
|
s->dist_htree_index = s->dist_context_map_slice[s->distance_context];
|
|
1305
|
-
return
|
|
1312
|
+
return BROTLI_DECODER_SUCCESS;
|
|
1306
1313
|
}
|
|
1307
1314
|
|
|
1308
|
-
static
|
|
1309
|
-
|
|
1315
|
+
static BROTLI_NOINLINE BrotliDecoderErrorCode
|
|
1316
|
+
DecodeDistanceBlockSwitch(BrotliDecoderState* s) {
|
|
1317
|
+
return DecodeDistanceBlockSwitchInternal(0, s);
|
|
1310
1318
|
}
|
|
1311
1319
|
|
|
1312
|
-
static
|
|
1320
|
+
static BROTLI_NOINLINE BrotliDecoderErrorCode SafeDecodeDistanceBlockSwitch(
|
|
1313
1321
|
BrotliDecoderState* s) {
|
|
1314
1322
|
return DecodeDistanceBlockSwitchInternal(1, s);
|
|
1315
1323
|
}
|
|
@@ -1940,6 +1948,9 @@ static BROTLI_INLINE BROTLI_BOOL CheckInputAmount(
|
|
|
1940
1948
|
return BrotliCheckInputAmount(br);
|
|
1941
1949
|
}
|
|
1942
1950
|
|
|
1951
|
+
/* NB: METHOD should return BROTLI_FALSE only in case there is not enough input;
|
|
1952
|
+
in case of "unsafe" execution, when input is guaranteed to be sufficient,
|
|
1953
|
+
result is ignored. */
|
|
1943
1954
|
#define BROTLI_SAFE(METHOD) \
|
|
1944
1955
|
{ \
|
|
1945
1956
|
if (safe) { \
|
|
@@ -1952,6 +1963,22 @@ static BROTLI_INLINE BROTLI_BOOL CheckInputAmount(
|
|
|
1952
1963
|
} \
|
|
1953
1964
|
}
|
|
1954
1965
|
|
|
1966
|
+
/* NB: METHOD should return BROTLI_DECODER_SUCCESS, BROTLI_DECODER_ERROR_*, or
|
|
1967
|
+
BROTLI_DECODER_NEEDS_MORE_INPUT; the later two break the processing. */
|
|
1968
|
+
#define BROTLI_SAFE_WITH_STATUS(METHOD) \
|
|
1969
|
+
{ \
|
|
1970
|
+
BrotliDecoderErrorCode status; \
|
|
1971
|
+
if (safe) { \
|
|
1972
|
+
status = Safe##METHOD; \
|
|
1973
|
+
} else { \
|
|
1974
|
+
status = METHOD; \
|
|
1975
|
+
} \
|
|
1976
|
+
if (status != BROTLI_DECODER_SUCCESS) { \
|
|
1977
|
+
result = status; \
|
|
1978
|
+
goto saveStateAndReturn; \
|
|
1979
|
+
} \
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1955
1982
|
static BROTLI_INLINE BrotliDecoderErrorCode ProcessCommandsInternal(
|
|
1956
1983
|
int safe, BrotliDecoderState* s) {
|
|
1957
1984
|
int pos = s->pos;
|
|
@@ -1991,7 +2018,7 @@ CommandBegin:
|
|
|
1991
2018
|
goto saveStateAndReturn;
|
|
1992
2019
|
}
|
|
1993
2020
|
if (BROTLI_PREDICT_FALSE(s->block_length[1] == 0)) {
|
|
1994
|
-
|
|
2021
|
+
BROTLI_SAFE_WITH_STATUS(DecodeCommandBlockSwitch(s));
|
|
1995
2022
|
goto CommandBegin;
|
|
1996
2023
|
}
|
|
1997
2024
|
/* Read the insert/copy length in the command. */
|
|
@@ -2136,7 +2163,7 @@ CommandPostDecodeLiterals:
|
|
|
2136
2163
|
} else {
|
|
2137
2164
|
/* Read distance code in the command, unless it was implicitly zero. */
|
|
2138
2165
|
if (BROTLI_PREDICT_FALSE(s->block_length[2] == 0)) {
|
|
2139
|
-
|
|
2166
|
+
BROTLI_SAFE_WITH_STATUS(DecodeDistanceBlockSwitch(s));
|
|
2140
2167
|
}
|
|
2141
2168
|
BROTLI_SAFE(ReadDistance(s, br));
|
|
2142
2169
|
}
|
|
@@ -2330,7 +2357,7 @@ CommandPostWrapCopy:
|
|
|
2330
2357
|
}
|
|
2331
2358
|
|
|
2332
2359
|
NextLiteralBlock:
|
|
2333
|
-
|
|
2360
|
+
BROTLI_SAFE_WITH_STATUS(DecodeLiteralBlockSwitch(s));
|
|
2334
2361
|
goto CommandInner;
|
|
2335
2362
|
|
|
2336
2363
|
saveStateAndReturn:
|
|
@@ -2552,6 +2579,8 @@ BrotliDecoderResult BrotliDecoderDecompressStream(
|
|
|
2552
2579
|
if (result != BROTLI_DECODER_SUCCESS) {
|
|
2553
2580
|
break;
|
|
2554
2581
|
}
|
|
2582
|
+
BROTLI_DCHECK(s->meta_block_remaining_len <=
|
|
2583
|
+
(int)BROTLI_BLOCK_SIZE_CAP);
|
|
2555
2584
|
BROTLI_LOG_UINT(s->is_last_metablock);
|
|
2556
2585
|
BROTLI_LOG_UINT(s->meta_block_remaining_len);
|
|
2557
2586
|
BROTLI_LOG_UINT(s->is_metadata);
|
|
@@ -85,9 +85,7 @@ typedef enum {
|
|
|
85
85
|
BROTLI_ERROR_CODE(_ERROR_FORMAT_, PADDING_1, -14) SEPARATOR \
|
|
86
86
|
BROTLI_ERROR_CODE(_ERROR_FORMAT_, PADDING_2, -15) SEPARATOR \
|
|
87
87
|
BROTLI_ERROR_CODE(_ERROR_FORMAT_, DISTANCE, -16) SEPARATOR \
|
|
88
|
-
|
|
89
|
-
/* -17 code is reserved */ \
|
|
90
|
-
\
|
|
88
|
+
BROTLI_ERROR_CODE(_ERROR_FORMAT_, BLOCK_SWITCH, -17) SEPARATOR \
|
|
91
89
|
BROTLI_ERROR_CODE(_ERROR_, COMPOUND_DICTIONARY, -18) SEPARATOR \
|
|
92
90
|
BROTLI_ERROR_CODE(_ERROR_, DICTIONARY_NOT_SET, -19) SEPARATOR \
|
|
93
91
|
BROTLI_ERROR_CODE(_ERROR_, INVALID_ARGUMENTS, -20) SEPARATOR \
|
|
@@ -12,6 +12,9 @@ end
|
|
|
12
12
|
module MultiCompress
|
|
13
13
|
module ActiveRecordSupport
|
|
14
14
|
class Type < ActiveModel::Type::Value
|
|
15
|
+
attr_reader :codec, :mutable
|
|
16
|
+
private :codec, :mutable
|
|
17
|
+
|
|
15
18
|
def initialize(mutable: false, **codec_opts)
|
|
16
19
|
@codec = MultiCompress::Codec.new(**codec_opts)
|
|
17
20
|
@mutable = mutable
|
|
@@ -29,32 +32,35 @@ module MultiCompress
|
|
|
29
32
|
|
|
30
33
|
# DB -> in-memory (unwrap)
|
|
31
34
|
def deserialize(value)
|
|
32
|
-
|
|
35
|
+
codec.load(value)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# in-memory -> DB (compress)
|
|
36
39
|
def serialize(value)
|
|
37
|
-
|
|
40
|
+
codec.dump(value)
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def changed_in_place?(raw_old_value, new_value)
|
|
41
|
-
return false unless
|
|
44
|
+
return false unless mutable
|
|
42
45
|
|
|
43
46
|
deserialize(raw_old_value) != new_value
|
|
44
47
|
end
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
class Coder
|
|
51
|
+
attr_reader :codec
|
|
52
|
+
private :codec
|
|
53
|
+
|
|
48
54
|
def initialize(**codec_opts)
|
|
49
55
|
@codec = MultiCompress::Codec.new(**codec_opts)
|
|
50
56
|
end
|
|
51
57
|
|
|
52
58
|
def dump(value)
|
|
53
|
-
|
|
59
|
+
codec.dump(value)
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
def load(value)
|
|
57
|
-
|
|
63
|
+
codec.load(value)
|
|
58
64
|
end
|
|
59
65
|
end
|
|
60
66
|
end
|
data/lib/multi_compress/codec.rb
CHANGED
|
@@ -4,51 +4,88 @@ require "multi_compress"
|
|
|
4
4
|
|
|
5
5
|
module MultiCompress
|
|
6
6
|
class Codec
|
|
7
|
-
MAGIC = "\x89MCDB".b
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
11
|
VERSION = 1
|
|
12
12
|
HEADER_SIZE = MAGIC.bytesize + 2 # magic + version byte + algo byte
|
|
13
13
|
B64_PREFIX = "mc1:"
|
|
14
|
-
MAX_NATIVE_OUTPUT = (2**(0.size * 8 - 1)) - 1
|
|
14
|
+
MAX_NATIVE_OUTPUT = (2**((0.size * 8) - 1)) - 1
|
|
15
15
|
|
|
16
16
|
ALGO_ID = { zstd: 1, lz4: 2, brotli: 3 }.freeze
|
|
17
17
|
ID_ALGO = ALGO_ID.invert.freeze
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Options = Struct.new(
|
|
20
|
+
:algo, :level, :encode, :serializer, :encoding, :max_output_size, :legacy, :dictionary,
|
|
21
|
+
keyword_init: true
|
|
22
|
+
)
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
DEFAULT_OPTIONS = {
|
|
25
|
+
algo: :zstd, level: nil, encode: nil, serializer: nil, encoding: Encoding::UTF_8,
|
|
26
|
+
max_output_size: nil, legacy: :reject, dictionary: nil
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
private_constant :DEFAULT_OPTIONS, :Options
|
|
30
|
+
|
|
31
|
+
def initialize(**values)
|
|
32
|
+
resolved_options = DEFAULT_OPTIONS.merge(values)
|
|
33
|
+
@options = Options.new(**resolved_options)
|
|
34
|
+
validate_options!
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def algo
|
|
38
|
+
@options.algo
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def level
|
|
42
|
+
@options.level
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def max_output_size
|
|
46
|
+
@options.max_output_size
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def validate_options!
|
|
52
|
+
validate_algorithm!
|
|
53
|
+
validate_encoding!
|
|
54
|
+
@options.max_output_size = normalized_output_limit
|
|
55
|
+
@options.legacy = normalize_legacy(@options.legacy)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_algorithm!
|
|
24
59
|
raise ArgumentError, "unknown algo #{algo.inspect}" unless ALGO_ID.key?(algo)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def validate_encoding!
|
|
63
|
+
return if [nil, :base64].include?(@options.encode)
|
|
64
|
+
|
|
65
|
+
raise ArgumentError, "encode must be nil or :base64"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def normalized_output_limit
|
|
69
|
+
limit = Integer(@options.max_output_size || MultiCompress.config.max_output_size)
|
|
33
70
|
raise ArgumentError, "max_output_size must be greater than 0" unless limit.positive?
|
|
34
71
|
if limit > MAX_NATIVE_OUTPUT
|
|
35
|
-
raise ArgumentError,
|
|
36
|
-
"max_output_size must be at most #{MAX_NATIVE_OUTPUT} bytes on this platform"
|
|
72
|
+
raise ArgumentError, "max_output_size must be at most #{MAX_NATIVE_OUTPUT} bytes on this platform"
|
|
37
73
|
end
|
|
38
74
|
|
|
39
|
-
|
|
40
|
-
@legacy = normalize_legacy(legacy)
|
|
41
|
-
@dictionary = dictionary
|
|
75
|
+
limit
|
|
42
76
|
end
|
|
43
77
|
|
|
78
|
+
public
|
|
79
|
+
|
|
44
80
|
def dump(value)
|
|
45
81
|
return nil if value.nil?
|
|
46
82
|
|
|
47
83
|
payload = serialize_payload(value)
|
|
48
|
-
body = MultiCompress.compress(payload, algo: @algo, level: @level,
|
|
84
|
+
body = MultiCompress.compress(payload, algo: @options.algo, level: @options.level,
|
|
85
|
+
dictionary: @options.dictionary)
|
|
49
86
|
framed = header + body
|
|
50
87
|
|
|
51
|
-
@encode == :base64 ? B64_PREFIX + [framed].pack("m0") : framed
|
|
88
|
+
@options.encode == :base64 ? B64_PREFIX + [framed].pack("m0") : framed
|
|
52
89
|
end
|
|
53
90
|
|
|
54
91
|
def load(stored)
|
|
@@ -60,105 +97,138 @@ module MultiCompress
|
|
|
60
97
|
read_legacy(stored)
|
|
61
98
|
end
|
|
62
99
|
|
|
63
|
-
|
|
100
|
+
module Envelope
|
|
101
|
+
private
|
|
64
102
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
def header
|
|
104
|
+
(MAGIC + [VERSION, ALGO_ID.fetch(@options.algo)].pack("C2")).b
|
|
105
|
+
end
|
|
68
106
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
def decode_transport(stored)
|
|
108
|
+
if @options.encode == :base64
|
|
109
|
+
s = stored.to_s
|
|
110
|
+
return nil unless s.start_with?(B64_PREFIX)
|
|
73
111
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
decoded = optional_base64(s.byteslice(B64_PREFIX.bytesize, s.bytesize - B64_PREFIX.bytesize))
|
|
113
|
+
decoded&.start_with?(MAGIC) ? decoded : nil
|
|
114
|
+
else
|
|
115
|
+
b = stored.b
|
|
116
|
+
b.start_with?(MAGIC) ? b : nil
|
|
117
|
+
end
|
|
79
118
|
end
|
|
80
|
-
end
|
|
81
119
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
120
|
+
def unwrap(framed)
|
|
121
|
+
raise MultiCompress::DataError, "MultiCompress::Codec: truncated envelope" if framed.bytesize < HEADER_SIZE
|
|
122
|
+
|
|
123
|
+
validate_envelope_version!(framed)
|
|
124
|
+
algo = envelope_algorithm(framed)
|
|
125
|
+
body = envelope_body(framed)
|
|
126
|
+
MultiCompress.decompress(body, algo: algo, dictionary: @options.dictionary,
|
|
127
|
+
max_output_size: @options.max_output_size)
|
|
85
128
|
end
|
|
86
129
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
130
|
+
def validate_envelope_version!(framed)
|
|
131
|
+
version = framed.getbyte(MAGIC.bytesize)
|
|
132
|
+
return if version == VERSION
|
|
133
|
+
|
|
90
134
|
raise MultiCompress::DataError, "MultiCompress::Codec: unsupported envelope version #{version}"
|
|
91
135
|
end
|
|
92
|
-
algo = ID_ALGO[algo_id]
|
|
93
|
-
raise MultiCompress::DataError, "MultiCompress::Codec: unknown algorithm id #{algo_id}" unless algo
|
|
94
136
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
137
|
+
def envelope_algorithm(framed)
|
|
138
|
+
algo_id = framed.getbyte(MAGIC.bytesize + 1)
|
|
139
|
+
ID_ALGO.fetch(algo_id) do
|
|
140
|
+
raise MultiCompress::DataError, "MultiCompress::Codec: unknown algorithm id #{algo_id}"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
98
143
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
144
|
+
def envelope_body(framed)
|
|
145
|
+
framed.byteslice(HEADER_SIZE, framed.bytesize - HEADER_SIZE) || +"".b
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def read_legacy(stored)
|
|
149
|
+
case @options.legacy[:mode]
|
|
150
|
+
when :reject
|
|
151
|
+
raise MultiCompress::DataError,
|
|
152
|
+
"MultiCompress::Codec: value has no envelope and legacy: :reject is set"
|
|
153
|
+
when :plain
|
|
154
|
+
decode_payload(stored.b.dup)
|
|
155
|
+
when :compressed
|
|
156
|
+
decode_legacy_compressed(stored)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def decode_legacy_compressed(stored)
|
|
107
161
|
payload = MultiCompress.decompress(
|
|
108
|
-
stored.b,
|
|
162
|
+
stored.b,
|
|
163
|
+
algo: @options.legacy[:algo],
|
|
164
|
+
dictionary: @options.dictionary,
|
|
165
|
+
max_output_size: @options.max_output_size
|
|
109
166
|
)
|
|
110
167
|
decode_payload(payload)
|
|
111
168
|
end
|
|
112
|
-
end
|
|
113
169
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
170
|
+
def normalize_legacy(legacy)
|
|
171
|
+
case legacy
|
|
172
|
+
when :reject, :plain
|
|
173
|
+
{ mode: legacy }
|
|
174
|
+
when Hash
|
|
175
|
+
algo = legacy[:compressed]
|
|
176
|
+
raise ArgumentError, "legacy: { compressed: <algo> } required" unless ALGO_ID.key?(algo)
|
|
177
|
+
|
|
178
|
+
{ mode: :compressed, algo: algo }
|
|
179
|
+
else
|
|
180
|
+
raise ArgumentError, "legacy: must be :reject, :plain, or { compressed: :algo }"
|
|
181
|
+
end
|
|
125
182
|
end
|
|
126
183
|
end
|
|
127
184
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
module Payload
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def serialize_payload(value)
|
|
189
|
+
if @options.serializer
|
|
190
|
+
@options.serializer.dump(value).b
|
|
191
|
+
else
|
|
192
|
+
serialize_string(value)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def serialize_string(value)
|
|
132
197
|
unless value.is_a?(String)
|
|
133
198
|
raise TypeError, "MultiCompress::Codec expects a String (or pass serializer:); got #{value.class}"
|
|
134
199
|
end
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
raise ArgumentError, "MultiCompress::Codec: value is not valid #{@encoding} " \
|
|
138
|
-
"(use encoding: Encoding::BINARY for arbitrary bytes)"
|
|
200
|
+
if @options.encoding == Encoding::BINARY || value.dup.force_encoding(@options.encoding).valid_encoding?
|
|
201
|
+
return value.b
|
|
139
202
|
end
|
|
140
|
-
|
|
203
|
+
|
|
204
|
+
raise ArgumentError, "MultiCompress::Codec: value is not valid #{@options.encoding} " \
|
|
205
|
+
"(use encoding: Encoding::BINARY for arbitrary bytes)"
|
|
141
206
|
end
|
|
142
|
-
end
|
|
143
207
|
|
|
144
|
-
|
|
145
|
-
|
|
208
|
+
def decode_payload(bytes)
|
|
209
|
+
return @options.serializer.load(bytes) if @options.serializer
|
|
146
210
|
|
|
147
|
-
|
|
148
|
-
|
|
211
|
+
finalize_string(bytes)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def finalize_string(bytes)
|
|
215
|
+
s = bytes.dup.force_encoding(@options.encoding)
|
|
216
|
+
unless @options.encoding == Encoding::BINARY || s.valid_encoding?
|
|
217
|
+
raise MultiCompress::DataError, "MultiCompress::Codec: decoded bytes are not valid #{@options.encoding}"
|
|
218
|
+
end
|
|
149
219
|
|
|
150
|
-
|
|
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}"
|
|
220
|
+
s
|
|
154
221
|
end
|
|
155
|
-
s
|
|
156
|
-
end
|
|
157
222
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
223
|
+
def optional_base64(str)
|
|
224
|
+
str.unpack1("m0")&.b
|
|
225
|
+
rescue ArgumentError
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
162
228
|
end
|
|
229
|
+
|
|
230
|
+
private_constant :Envelope, :Payload
|
|
231
|
+
include Envelope
|
|
232
|
+
include Payload
|
|
163
233
|
end
|
|
164
234
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiCompress
|
|
4
|
+
module Database
|
|
5
|
+
ENGINE = Engine.new
|
|
6
|
+
private_constant :ENGINE
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def compress(text, dictionary: nil)
|
|
11
|
+
ENGINE.compress(text, dictionary: dictionary)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compress_v1(text)
|
|
15
|
+
ENGINE.compress_v1(text)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def compress_v2(text, dictionary:)
|
|
19
|
+
ENGINE.compress_v2(text, dictionary: dictionary)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def decompress(blob, dictionary: nil)
|
|
23
|
+
ENGINE.decompress(blob, dictionary: dictionary)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def valid?(blob, dictionary: nil)
|
|
27
|
+
ENGINE.valid?(blob, dictionary: dictionary)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dictionary_ref(blob)
|
|
31
|
+
ENGINE.dictionary_ref(blob)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def original_size(blob)
|
|
35
|
+
ENGINE.original_size(blob)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def version_string
|
|
39
|
+
ENGINE.version_string
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|