From 1fb430ba46b71c0b8ceba7b86dbf00dad7d678db Mon Sep 17 00:00:00 2001 From: kjdev Date: Fri, 8 Aug 2025 10:43:21 +0900 Subject: [PATCH 1/4] fix: simplify output handler context start by removing intermediate variable and adding compression coding check --- zstd.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zstd.c b/zstd.c index d44191a..67b25de 100644 --- a/zstd.c +++ b/zstd.c @@ -1320,12 +1320,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) From 4cc370f7ba61d9beaed026150cae64bd69bc3fd0 Mon Sep 17 00:00:00 2001 From: kjdev Date: Fri, 8 Aug 2025 10:44:45 +0900 Subject: [PATCH 2/4] chore: add binary attributes for zstd and dict files in gitattributes --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) 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 From 5c6d757c03b255a8e31569d21a678dedf0ade4d2 Mon Sep 17 00:00:00 2001 From: kjdev Date: Fri, 8 Aug 2025 09:10:02 +0900 Subject: [PATCH 3/4] experimental: dcz support in output handler --- zstd.c | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/zstd.c b/zstd.c index 67b25de..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; } @@ -1347,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; @@ -1430,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; } @@ -1449,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); } From ff9a7290c1c292011e59b6430a5474f2c914db94 Mon Sep 17 00:00:00 2001 From: kjdev Date: Fri, 8 Aug 2025 10:44:23 +0900 Subject: [PATCH 4/4] test: add dcz output handler --- tests/files/ob_data.zstd | Bin 0 -> 1839 bytes tests/files/ob_dcz.zstd | Bin 0 -> 1872 bytes tests/ob_dcz_001.phpt | 24 ++++++++++++++++++++++++ tests/ob_dcz_002.phpt | 20 ++++++++++++++++++++ tests/ob_dcz_003.phpt | 22 ++++++++++++++++++++++ tests/ob_dcz_004.phpt | 24 ++++++++++++++++++++++++ 6 files changed, 90 insertions(+) create mode 100644 tests/files/ob_data.zstd create mode 100644 tests/files/ob_dcz.zstd create mode 100644 tests/ob_dcz_001.phpt create mode 100644 tests/ob_dcz_002.phpt create mode 100644 tests/ob_dcz_003.phpt create mode 100644 tests/ob_dcz_004.phpt diff --git a/tests/files/ob_data.zstd b/tests/files/ob_data.zstd new file mode 100644 index 0000000000000000000000000000000000000000..90f4f959e56c55095705bee08f1d18b5232fe25a GIT binary patch literal 1839 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 KjBqGq$CwpYqqjo< literal 0 HcmV?d00001 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