diff --git a/.gitattributes b/.gitattributes index ffc3195..621dcf6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ *.inc -text *.phpt -text +*.zstd binary +*.dic binary diff --git a/tests/files/ob_data.zstd b/tests/files/ob_data.zstd new file mode 100644 index 0000000..90f4f95 Binary files /dev/null and b/tests/files/ob_data.zstd differ diff --git a/tests/files/ob_dcz.zstd b/tests/files/ob_dcz.zstd new file mode 100644 index 0000000..c7b0081 Binary files /dev/null and b/tests/files/ob_dcz.zstd differ diff --git a/tests/ob_dcz_001.phpt b/tests/ob_dcz_001.phpt new file mode 100644 index 0000000..df69ba7 --- /dev/null +++ b/tests/ob_dcz_001.phpt @@ -0,0 +1,24 @@ +--TEST-- +output handler: dcz +--SKIPIF-- + +--GET-- +ob=dictionary +--ENV-- +HTTP_ACCEPT_ENCODING=dcz +HTTP_AVAILABLE_DICTIONARY=:5wg7BLZeirApJAxOdI/QBi8RvwZuIJfPf0TwMo/x/yg=: +--FILE-- + +--EXPECT_EXTERNAL-- +files/ob_dcz.zstd +--EXPECTHEADERS-- +Content-Encoding: dcz +Vary: Accept-Encoding, Available-Dictionary diff --git a/tests/ob_dcz_002.phpt b/tests/ob_dcz_002.phpt new file mode 100644 index 0000000..d1c7a4c --- /dev/null +++ b/tests/ob_dcz_002.phpt @@ -0,0 +1,20 @@ +--TEST-- +output handler: dcz: invalid available-dictionary +--SKIPIF-- + +--GET-- +ob=dictionary +--ENV-- +HTTP_ACCEPT_ENCODING=dcz +--FILE-- + +--GET-- +ob=dictionary +--ENV-- +HTTP_ACCEPT_ENCODING=dcz +HTTP_AVAILABLE_DICTIONARY=:test: +--FILE-- + +--EXPECTF-- +%a +Warning: %s: zstd: invalid available-dictionary: request(:test:) != actual(%s) in Unknown on line 0 diff --git a/tests/ob_dcz_004.phpt b/tests/ob_dcz_004.phpt new file mode 100644 index 0000000..eb53bf1 --- /dev/null +++ b/tests/ob_dcz_004.phpt @@ -0,0 +1,24 @@ +--TEST-- +output handler: zstd,dcz: invalid available-dictionary +--SKIPIF-- + +--GET-- +ob=dictionary +--ENV-- +HTTP_ACCEPT_ENCODING=dcz,zstd +HTTP_AVAILABLE_DICTIONARY=:test: +--FILE-- + +--EXPECT_EXTERNAL-- +files/ob_data.zstd +--EXPECTHEADERS-- +Content-Encoding: zstd +Vary: Accept-Encoding diff --git a/zstd.c b/zstd.c index d44191a..6623573 100644 --- a/zstd.c +++ b/zstd.c @@ -28,6 +28,9 @@ #include #include #include +#include +#include +#include #include #include #if PHP_VERSION_ID < 70200 @@ -80,6 +83,7 @@ struct _php_zstd_context { ZSTD_DDict *ddict; ZSTD_inBuffer input; ZSTD_outBuffer output; + zend_uchar dict_digest[32]; zend_object std; }; @@ -107,6 +111,7 @@ static void php_zstd_context_init(php_zstd_context *ctx) ctx->output.dst = NULL; ctx->output.size = 0; ctx->output.pos = 0; + memset(ctx->dict_digest, 0, sizeof(ctx->dict_digest)); } static void php_zstd_context_free(php_zstd_context *ctx) @@ -1253,6 +1258,9 @@ static int APC_UNSERIALIZER_NAME(zstd)(APC_UNSERIALIZER_ARGS) #if PHP_VERSION_ID >= 80000 #define PHP_ZSTD_OUTPUT_HANDLER_NAME "zstd output compression" +#define PHP_ZSTD_ENCODING_ZSTD (1 << 0) +#define PHP_ZSTD_ENCODING_DCZ (1 << 1) + static int php_zstd_output_encoding(void) { zval *enc; @@ -1270,7 +1278,10 @@ static int php_zstd_output_encoding(void) sizeof("HTTP_ACCEPT_ENCODING") - 1))) { convert_to_string(enc); if (strstr(Z_STRVAL_P(enc), "zstd")) { - PHP_ZSTD_G(compression_coding) = 1; + PHP_ZSTD_G(compression_coding) = PHP_ZSTD_ENCODING_ZSTD; + } + if (strstr(Z_STRVAL_P(enc), "dcz")) { + PHP_ZSTD_G(compression_coding) |= PHP_ZSTD_ENCODING_DCZ; } } } @@ -1308,6 +1319,50 @@ php_zstd_output_handler_load_dict(php_zstd_context *ctx) php_stream_close(stream); + if (!data) { + return NULL; + } + + if (PHP_ZSTD_G(compression_coding) & PHP_ZSTD_ENCODING_DCZ) { + zval *available; + if ((Z_TYPE(PG(http_globals)[TRACK_VARS_SERVER]) == IS_ARRAY + || zend_is_auto_global_str(ZEND_STRL("_SERVER"))) + && (available = zend_hash_str_find( + Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), + "HTTP_AVAILABLE_DICTIONARY", + sizeof("HTTP_AVAILABLE_DICTIONARY") - 1))) { + convert_to_string(available); + + PHP_SHA256_CTX context; + PHP_SHA256Init(&context); + PHP_SHA256Update(&context, ZSTR_VAL(data), ZSTR_LEN(data)); + PHP_SHA256Final(ctx->dict_digest, &context); + + zend_string *b64; + b64 = php_base64_encode(ctx->dict_digest, sizeof(ctx->dict_digest)); + if (b64) { + if (Z_STRLEN_P(available) <= ZSTR_LEN(b64) + || memcmp(ZSTR_VAL(b64), + Z_STRVAL_P(available) + 1, ZSTR_LEN(b64))) { + php_error_docref(NULL, E_WARNING, + "zstd: invalid available-dictionary: " + "request(%s) != actual(%s)", + Z_STRVAL_P(available), ZSTR_VAL(b64)); + PHP_ZSTD_G(compression_coding) &= ~PHP_ZSTD_ENCODING_DCZ; + zend_string_release(data); + data = NULL; + } + zend_string_free(b64); + } + } else { + php_error_docref(NULL, E_WARNING, + "zstd: not found available-dictionary"); + PHP_ZSTD_G(compression_coding) &= ~PHP_ZSTD_ENCODING_DCZ; + zend_string_release(data); + data = NULL; + } + } + return data; } @@ -1320,12 +1375,11 @@ static zend_result php_zstd_output_handler_context_start(php_zstd_context *ctx) } zend_string *dict = php_zstd_output_handler_load_dict(ctx); - - if (php_zstd_context_create_compress(ctx, level, dict) != SUCCESS) { + if (!PHP_ZSTD_G(compression_coding)) { return FAILURE; } - return SUCCESS; + return php_zstd_context_create_compress(ctx, level, dict); } static void php_zstd_output_handler_context_dtor(void *opaq) @@ -1348,9 +1402,19 @@ php_zstd_output_handler_write(php_zstd_context *ctx, if (output_context->out.size < ctx->output.size) { output_context->out.size = ctx->output.size; } - output_context->out.data = emalloc(output_context->out.size); + + if ((output_context->op & PHP_OUTPUT_HANDLER_START) + && (PHP_ZSTD_G(compression_coding) & PHP_ZSTD_ENCODING_DCZ)) { + output_context->out.size += 40; + output_context->out.data = emalloc(output_context->out.size); + memcpy(output_context->out.data, "\x5e\x2a\x4d\x18\x20\x00\x00\x00", 8); + memcpy(output_context->out.data + 8, ctx->dict_digest, 32); + output_context->out.used = 40; + } else { + output_context->out.data = emalloc(output_context->out.size); + output_context->out.used = 0; + } output_context->out.free = 1; - output_context->out.used = 0; do { ctx->output.pos = 0; @@ -1431,7 +1495,13 @@ php_zstd_output_handler(void **handler_context, && (output_context->op != (PHP_OUTPUT_HANDLER_START |PHP_OUTPUT_HANDLER_CLEAN |PHP_OUTPUT_HANDLER_FINAL))) { - sapi_add_header_ex(ZEND_STRL("Vary: Accept-Encoding"), 1, 0); + if (PHP_ZSTD_G(compression_coding) & PHP_ZSTD_ENCODING_DCZ) { + sapi_add_header_ex( + ZEND_STRL("Vary: Accept-Encoding, Available-Dictionary"), + 1, 0); + } else { + sapi_add_header_ex(ZEND_STRL("Vary: Accept-Encoding"), 1, 0); + } } return FAILURE; } @@ -1450,8 +1520,18 @@ php_zstd_output_handler(void **handler_context, if (SG(headers_sent) || !PHP_ZSTD_G(output_compression)) { return FAILURE; } - sapi_add_header_ex(ZEND_STRL("Content-Encoding: zstd"), 1, 1); - sapi_add_header_ex(ZEND_STRL("Vary: Accept-Encoding"), 1, 0); + if (PHP_ZSTD_G(compression_coding) & PHP_ZSTD_ENCODING_DCZ) { + sapi_add_header_ex(ZEND_STRL("Content-Encoding: dcz"), + 1, 1); + sapi_add_header_ex(ZEND_STRL("Vary: Accept-Encoding, " + "Available-Dictionary"), + 1, 0); + } else { + sapi_add_header_ex(ZEND_STRL("Content-Encoding: zstd"), + 1, 1); + sapi_add_header_ex(ZEND_STRL("Vary: Accept-Encoding"), + 1, 0); + } php_output_handler_hook(PHP_OUTPUT_HANDLER_HOOK_IMMUTABLE, NULL); }