feat(codec): implement md4/sha1/none digests and lz4/zlib/zlibx codecs
Add real implementations for the rsync 3.4.1 checksum and compression breadth: a self-contained MD4 (RFC 1320), OpenSSL-backed SHA1, a no-digest mode, and LZ4/zlib codecs alongside zstd. Compressed buffers are now self-describing (a leading codec id), so every existing decompression call site keeps working through a process-global codec selection. zlibx shares the zlib codec because FastSync compresses only delta/token bytes (never matched file data), matching the 'x' intent.
This commit is contained in:
+298
-24
@@ -3,12 +3,15 @@
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include <limits.h>
|
||||
#include <lz4.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <threads.h>
|
||||
#include <unistd.h>
|
||||
#include <zlib.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024)
|
||||
@@ -29,6 +32,13 @@
|
||||
"z " \
|
||||
"zip zst"
|
||||
|
||||
/* Self-describing compressed frames: the first byte is the CompressionAlgo id.
|
||||
* zlib/lz4 store the uncompressed size as a little-endian uint32 after the
|
||||
* codec byte so decompression can be exactly pre-sized and bounded. */
|
||||
#define LZ4_SIZE_PREFIX_LEN 4
|
||||
|
||||
static _Atomic int g_compression_algo = COMPRESSION_ALGO_ZSTD;
|
||||
|
||||
/* Case-insensitive match of a bare suffix (no leading dot) against a
|
||||
* space-separated suffix list. */
|
||||
static bool suffix_in_list(const char* name, const char* list) {
|
||||
@@ -67,6 +77,75 @@ bool compression_should_skip_with_suffixes(const char* path, char* const* suffix
|
||||
return false;
|
||||
}
|
||||
|
||||
CompressionAlgo compression_default_algo(void) {
|
||||
return COMPRESSION_ALGO_ZSTD;
|
||||
}
|
||||
|
||||
int compression_algo_from_name(const char* name) {
|
||||
if (!name)
|
||||
return -1;
|
||||
if (strcasecmp(name, "zstd") == 0)
|
||||
return (int)COMPRESSION_ALGO_ZSTD;
|
||||
if (strcasecmp(name, "lz4") == 0)
|
||||
return (int)COMPRESSION_ALGO_LZ4;
|
||||
if (strcasecmp(name, "zlib") == 0)
|
||||
return (int)COMPRESSION_ALGO_ZLIB;
|
||||
if (strcasecmp(name, "zlibx") == 0)
|
||||
return (int)COMPRESSION_ALGO_ZLIBX;
|
||||
if (strcasecmp(name, "none") == 0)
|
||||
return (int)COMPRESSION_ALGO_NONE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* compression_algo_name(CompressionAlgo algo) {
|
||||
switch (algo) {
|
||||
case COMPRESSION_ALGO_NONE:
|
||||
return "none";
|
||||
case COMPRESSION_ALGO_ZSTD:
|
||||
return "zstd";
|
||||
case COMPRESSION_ALGO_LZ4:
|
||||
return "lz4";
|
||||
case COMPRESSION_ALGO_ZLIB:
|
||||
return "zlib";
|
||||
case COMPRESSION_ALGO_ZLIBX:
|
||||
return "zlibx";
|
||||
}
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
bool compression_algo_valid(int algo) {
|
||||
return algo == (int)COMPRESSION_ALGO_NONE || algo == (int)COMPRESSION_ALGO_ZSTD ||
|
||||
algo == (int)COMPRESSION_ALGO_LZ4 || algo == (int)COMPRESSION_ALGO_ZLIB ||
|
||||
algo == (int)COMPRESSION_ALGO_ZLIBX;
|
||||
}
|
||||
|
||||
bool compression_algo_enabled(CompressionAlgo algo) {
|
||||
return algo != COMPRESSION_ALGO_NONE;
|
||||
}
|
||||
|
||||
CompressionAlgo compression_negotiate_default(void) {
|
||||
/* rsync 3.4.1 default preference order; every entry is compiled in, so this
|
||||
* resolves to zstd. */
|
||||
static const CompressionAlgo preference[] = {
|
||||
COMPRESSION_ALGO_ZSTD, COMPRESSION_ALGO_LZ4, COMPRESSION_ALGO_ZLIBX,
|
||||
COMPRESSION_ALGO_ZLIB, COMPRESSION_ALGO_NONE,
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(preference) / sizeof(preference[0]); i++) {
|
||||
if (compression_algo_valid((int)preference[i]))
|
||||
return preference[i];
|
||||
}
|
||||
return COMPRESSION_ALGO_ZSTD;
|
||||
}
|
||||
|
||||
void compression_set_algo(CompressionAlgo algo) {
|
||||
if (compression_algo_valid((int)algo))
|
||||
atomic_store(&g_compression_algo, (int)algo);
|
||||
}
|
||||
|
||||
CompressionAlgo compression_get_algo(void) {
|
||||
return (CompressionAlgo)atomic_load(&g_compression_algo);
|
||||
}
|
||||
|
||||
/* Per-thread cache of zstd contexts plus the grow-only compression scratch
|
||||
* buffer. zstd contexts are stateful and not safe to share between threads,
|
||||
* so each thread keeps its own (see compression_get_thread_ctx). The cache is
|
||||
@@ -157,17 +236,25 @@ static void compression_ctx_put(CompressionThreadCtx* ctx) {
|
||||
compression_ctx_free(ctx);
|
||||
}
|
||||
|
||||
Data* data_compress(Data* data_to_compress, int compression_level) {
|
||||
return data_compress_with_threads(data_to_compress, compression_level, 0);
|
||||
/* Build a frame consisting of a copy of `src` prefixed by `codec`. */
|
||||
static Data* frame_with_codec(const void* src, size_t size, CompressionAlgo codec) {
|
||||
if (size > SIZE_MAX - 1)
|
||||
return NULL;
|
||||
Data* out = data_create_empty(size + 1);
|
||||
if (!out)
|
||||
return NULL;
|
||||
((uint8_t*)out->data)[0] = (uint8_t)codec;
|
||||
if (size > 0)
|
||||
memcpy((uint8_t*)out->data + 1, src, size);
|
||||
out->size = size + 1;
|
||||
return out;
|
||||
}
|
||||
|
||||
Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
|
||||
int compression_threads) {
|
||||
if (!data_to_compress || (!data_to_compress->data && data_to_compress->size != 0) ||
|
||||
compression_threads < 0 || compression_threads > COMPRESSION_MAX_THREADS)
|
||||
static Data* zstd_compress(Data* in, int compression_level, int compression_threads) {
|
||||
size_t dst_size = ZSTD_compressBound(in->size);
|
||||
if (dst_size > SIZE_MAX - 1)
|
||||
return NULL;
|
||||
log_message(LOG_LEVEL_DEBUG, "Starting to compress data");
|
||||
size_t dst_size = ZSTD_compressBound(data_to_compress->size);
|
||||
dst_size += 1; /* codec prefix */
|
||||
|
||||
CompressionThreadCtx* ctx = compression_get_thread_ctx();
|
||||
if (ctx == NULL) {
|
||||
@@ -218,7 +305,7 @@ Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
|
||||
|
||||
if (available_threads > 0) {
|
||||
/* Streaming compression needs the source size before threaded mode can end a frame. */
|
||||
size_t zret = ZSTD_CCtx_setPledgedSrcSize(ctx->cctx, data_to_compress->size);
|
||||
size_t zret = ZSTD_CCtx_setPledgedSrcSize(ctx->cctx, in->size);
|
||||
if (ZSTD_isError(zret)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to set compression source size: %s",
|
||||
ZSTD_getErrorName(zret));
|
||||
@@ -236,8 +323,8 @@ Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
|
||||
ctx->out_cap = dst_size;
|
||||
}
|
||||
|
||||
ZSTD_inBuffer input = {data_to_compress->data, data_to_compress->size, 0};
|
||||
ZSTD_outBuffer output = {ctx->out_buf, dst_size, 0};
|
||||
ZSTD_inBuffer input = {in->data, in->size, 0};
|
||||
ZSTD_outBuffer output = {(uint8_t*)ctx->out_buf + 1, dst_size - 1, 0};
|
||||
|
||||
size_t ret;
|
||||
do {
|
||||
@@ -250,30 +337,192 @@ Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
|
||||
|
||||
/* Hand off an exactly-sized copy; the scratch buffer stays cached so the next
|
||||
* call does not reallocate a ZSTD_compressBound-sized block. */
|
||||
compressed_data = data_create_empty(output.pos);
|
||||
compressed_data = data_create_empty(output.pos + 1);
|
||||
if (compressed_data == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to allocate compressed data");
|
||||
goto cleanup;
|
||||
}
|
||||
((uint8_t*)compressed_data->data)[0] = (uint8_t)COMPRESSION_ALGO_ZSTD;
|
||||
if (output.pos > 0)
|
||||
memcpy(compressed_data->data, ctx->out_buf, output.pos);
|
||||
compressed_data->size = output.pos;
|
||||
memcpy((uint8_t*)compressed_data->data + 1, (uint8_t*)ctx->out_buf + 1, output.pos);
|
||||
compressed_data->size = output.pos + 1;
|
||||
|
||||
log_debug_message(LOG_DEBUG_UTIL, "Data succesfully compressed from %zu to %zu",
|
||||
data_to_compress->size, compressed_data->size);
|
||||
log_debug_message(LOG_DEBUG_UTIL, "Data succesfully compressed from %zu to %zu", in->size,
|
||||
compressed_data->size);
|
||||
|
||||
cleanup:
|
||||
compression_ctx_put(ctx);
|
||||
return compressed_data;
|
||||
}
|
||||
|
||||
Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) {
|
||||
if (!compressed_data || (!compressed_data->data && compressed_data->size != 0) ||
|
||||
maximum_size == 0)
|
||||
static Data* lz4_compress(Data* in) {
|
||||
int bound = LZ4_compressBound((int)in->size);
|
||||
if (bound < 0 || in->size > (size_t)INT_MAX)
|
||||
return NULL;
|
||||
Data* out = data_create_empty((size_t)bound + 1 + LZ4_SIZE_PREFIX_LEN);
|
||||
if (!out)
|
||||
return NULL;
|
||||
uint32_t raw_size = (uint32_t)in->size;
|
||||
uint8_t* p = (uint8_t*)out->data;
|
||||
p[0] = (uint8_t)COMPRESSION_ALGO_LZ4;
|
||||
for (int i = 0; i < LZ4_SIZE_PREFIX_LEN; i++)
|
||||
p[1 + i] = (uint8_t)((raw_size >> (8 * i)) & 0xff);
|
||||
int written = 0;
|
||||
if (in->size > 0) {
|
||||
written = LZ4_compress_default((const char*)in->data, (char*)p + 1 + LZ4_SIZE_PREFIX_LEN,
|
||||
(int)in->size, bound);
|
||||
if (written <= 0) {
|
||||
data_destroy(out);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
out->size = (size_t)written + 1 + LZ4_SIZE_PREFIX_LEN;
|
||||
return out;
|
||||
}
|
||||
|
||||
static Data* zlib_compress(Data* in, CompressionAlgo algo, int compression_level) {
|
||||
int level = compression_level;
|
||||
if (level < 1)
|
||||
level = Z_DEFAULT_COMPRESSION;
|
||||
if (level > 9)
|
||||
level = 9;
|
||||
uLong bound = compressBound((uLong)in->size);
|
||||
if (in->size > (size_t)ULONG_MAX)
|
||||
return NULL;
|
||||
Data* out = data_create_empty((size_t)bound + 1 + LZ4_SIZE_PREFIX_LEN);
|
||||
if (!out)
|
||||
return NULL;
|
||||
uint32_t raw_size = (uint32_t)in->size;
|
||||
uint8_t* p = (uint8_t*)out->data;
|
||||
p[0] = (uint8_t)algo;
|
||||
for (int i = 0; i < LZ4_SIZE_PREFIX_LEN; i++)
|
||||
p[1 + i] = (uint8_t)((raw_size >> (8 * i)) & 0xff);
|
||||
uLongf dest_len = bound;
|
||||
int rc = compress2(p + 1 + LZ4_SIZE_PREFIX_LEN, &dest_len, (const Bytef*)in->data,
|
||||
(uLong)in->size, level);
|
||||
if (rc != Z_OK) {
|
||||
data_destroy(out);
|
||||
return NULL;
|
||||
}
|
||||
out->size = (size_t)dest_len + 1 + LZ4_SIZE_PREFIX_LEN;
|
||||
return out;
|
||||
}
|
||||
|
||||
Data* data_compress_codec(Data* data_to_compress, CompressionAlgo algo, int compression_level,
|
||||
int compression_threads) {
|
||||
if (!data_to_compress || (!data_to_compress->data && data_to_compress->size != 0) ||
|
||||
compression_threads < 0 || compression_threads > COMPRESSION_MAX_THREADS)
|
||||
return NULL;
|
||||
if (!compression_algo_valid((int)algo))
|
||||
return NULL;
|
||||
log_message(LOG_LEVEL_DEBUG, "Starting to compress data");
|
||||
switch (algo) {
|
||||
case COMPRESSION_ALGO_NONE:
|
||||
return frame_with_codec(data_to_compress->data, data_to_compress->size, COMPRESSION_ALGO_NONE);
|
||||
case COMPRESSION_ALGO_ZSTD:
|
||||
return zstd_compress(data_to_compress, compression_level, compression_threads);
|
||||
case COMPRESSION_ALGO_LZ4:
|
||||
return lz4_compress(data_to_compress);
|
||||
case COMPRESSION_ALGO_ZLIB:
|
||||
case COMPRESSION_ALGO_ZLIBX:
|
||||
return zlib_compress(data_to_compress, algo, compression_level);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
|
||||
int compression_threads) {
|
||||
return data_compress_codec(data_to_compress, compression_get_algo(), compression_level,
|
||||
compression_threads);
|
||||
}
|
||||
|
||||
Data* data_compress(Data* data_to_compress, int compression_level) {
|
||||
return data_compress_codec(data_to_compress, compression_get_algo(), compression_level, 0);
|
||||
}
|
||||
|
||||
static Data* decompress_none(const Data* compressed_data, size_t maximum_size) {
|
||||
size_t size = compressed_data->size - 1;
|
||||
if (size > maximum_size)
|
||||
return NULL;
|
||||
Data* out = data_create_empty(size);
|
||||
if (!out)
|
||||
return NULL;
|
||||
if (size > 0)
|
||||
memcpy(out->data, (const uint8_t*)compressed_data->data + 1, size);
|
||||
out->size = size;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Read the 4-byte little-endian raw size stored after the codec byte. */
|
||||
static bool read_raw_size(const Data* in, uint32_t* raw_size) {
|
||||
if (in->size < 1 + LZ4_SIZE_PREFIX_LEN)
|
||||
return false;
|
||||
const uint8_t* p = (const uint8_t*)in->data;
|
||||
uint32_t v = 0;
|
||||
for (int i = 0; i < LZ4_SIZE_PREFIX_LEN; i++)
|
||||
v |= (uint32_t)p[1 + i] << (8 * i);
|
||||
*raw_size = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
static Data* lz4_decompress(Data* compressed_data, size_t maximum_size, size_t hard_limit) {
|
||||
uint32_t raw_size = 0;
|
||||
if (!read_raw_size(compressed_data, &raw_size))
|
||||
return NULL;
|
||||
if (raw_size > hard_limit || raw_size > maximum_size)
|
||||
return NULL;
|
||||
size_t comp_size = compressed_data->size - 1 - LZ4_SIZE_PREFIX_LEN;
|
||||
Data* out = data_create_empty(raw_size);
|
||||
if (!out)
|
||||
return NULL;
|
||||
if (raw_size == 0) {
|
||||
out->size = 0;
|
||||
return out;
|
||||
}
|
||||
int rc = LZ4_decompress_safe((const char*)compressed_data->data + 1 + LZ4_SIZE_PREFIX_LEN,
|
||||
(char*)out->data, (int)comp_size, (int)raw_size);
|
||||
if (rc < 0 || (uint32_t)rc != raw_size) {
|
||||
log_message(LOG_LEVEL_ERROR, "LZ4 decompression failed");
|
||||
data_destroy(out);
|
||||
return NULL;
|
||||
}
|
||||
out->size = raw_size;
|
||||
return out;
|
||||
}
|
||||
|
||||
static Data* zlib_decompress(Data* compressed_data, size_t maximum_size, size_t hard_limit) {
|
||||
uint32_t raw_size = 0;
|
||||
if (!read_raw_size(compressed_data, &raw_size))
|
||||
return NULL;
|
||||
if (raw_size > hard_limit || raw_size > maximum_size)
|
||||
return NULL;
|
||||
size_t comp_size = compressed_data->size - 1 - LZ4_SIZE_PREFIX_LEN;
|
||||
Data* out = data_create_empty(raw_size);
|
||||
if (!out)
|
||||
return NULL;
|
||||
if (raw_size == 0) {
|
||||
out->size = 0;
|
||||
return out;
|
||||
}
|
||||
uLongf dest_len = raw_size;
|
||||
int rc =
|
||||
uncompress((Bytef*)out->data, &dest_len,
|
||||
(const Bytef*)compressed_data->data + 1 + LZ4_SIZE_PREFIX_LEN, (uLong)comp_size);
|
||||
if (rc != Z_OK || dest_len != raw_size) {
|
||||
log_message(LOG_LEVEL_ERROR, "zlib decompression failed");
|
||||
data_destroy(out);
|
||||
return NULL;
|
||||
}
|
||||
out->size = raw_size;
|
||||
return out;
|
||||
}
|
||||
|
||||
static Data* zstd_decompress(Data* compressed_data, size_t maximum_size) {
|
||||
/* The zstd frame starts after the codec byte. */
|
||||
const void* frame = (const uint8_t*)compressed_data->data + 1;
|
||||
size_t frame_size = compressed_data->size - 1;
|
||||
log_debug_message(LOG_DEBUG_UTIL, "Start to decompress data");
|
||||
unsigned long long dst_size =
|
||||
ZSTD_getFrameContentSize(compressed_data->data, compressed_data->size);
|
||||
unsigned long long dst_size = ZSTD_getFrameContentSize(frame, frame_size);
|
||||
/* ZSTD_isError() is also true for ZSTD_CONTENTSIZE_ERROR and
|
||||
* ZSTD_CONTENTSIZE_UNKNOWN (both are encoded near (size_t)-1), so test the
|
||||
* sentinels explicitly instead of blanket-rejecting every error-ish value:
|
||||
@@ -287,9 +536,9 @@ Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) {
|
||||
// ZSTD_CONTENTSIZE_UNKNOWN (~2^64) can cause massive allocation;
|
||||
// fall back to a conservative estimate (3x compressed size) when unknown.
|
||||
if (dst_size == ZSTD_CONTENTSIZE_UNKNOWN) {
|
||||
if (compressed_data->size > ULLONG_MAX / 3)
|
||||
if (frame_size > ULLONG_MAX / 3)
|
||||
return NULL;
|
||||
dst_size = compressed_data->size * 3;
|
||||
dst_size = frame_size * 3;
|
||||
if (dst_size < INITIAL_DECOMPRESS_BUF_SIZE)
|
||||
dst_size = INITIAL_DECOMPRESS_BUF_SIZE;
|
||||
}
|
||||
@@ -326,7 +575,7 @@ Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ZSTD_inBuffer input = {compressed_data->data, compressed_data->size, 0};
|
||||
ZSTD_inBuffer input = {frame, frame_size, 0};
|
||||
ZSTD_outBuffer output = {uncompressed_data->data, buf_size, 0};
|
||||
|
||||
size_t ret;
|
||||
@@ -385,6 +634,31 @@ cleanup:
|
||||
return uncompressed_data;
|
||||
}
|
||||
|
||||
Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) {
|
||||
if (!compressed_data || (!compressed_data->data && compressed_data->size != 0) ||
|
||||
maximum_size == 0)
|
||||
return NULL;
|
||||
if (compressed_data->size < 1)
|
||||
return NULL;
|
||||
unsigned long long hard_limit =
|
||||
maximum_size < MAX_DECOMPRESSED_SIZE ? maximum_size : MAX_DECOMPRESSED_SIZE;
|
||||
uint8_t codec = ((const uint8_t*)compressed_data->data)[0];
|
||||
if (!compression_algo_valid(codec))
|
||||
return NULL;
|
||||
switch ((CompressionAlgo)codec) {
|
||||
case COMPRESSION_ALGO_NONE:
|
||||
return decompress_none(compressed_data, (size_t)hard_limit);
|
||||
case COMPRESSION_ALGO_ZSTD:
|
||||
return zstd_decompress(compressed_data, (size_t)hard_limit);
|
||||
case COMPRESSION_ALGO_LZ4:
|
||||
return lz4_decompress(compressed_data, maximum_size, (size_t)hard_limit);
|
||||
case COMPRESSION_ALGO_ZLIB:
|
||||
case COMPRESSION_ALGO_ZLIBX:
|
||||
return zlib_decompress(compressed_data, maximum_size, (size_t)hard_limit);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Data* data_decompress(Data* compressed_data) {
|
||||
return data_decompress_limited(compressed_data, MAX_DECOMPRESSED_SIZE);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user