From a2727612bdce033d4aeaab668f8808a2015d5745 Mon Sep 17 00:00:00 2001 From: kjdev Date: Thu, 13 Aug 2026 08:33:15 +0900 Subject: [PATCH 1/3] feat: support negation tokens in output_compression_exclude_types A token prefixed with `!` negates a match against the built-in MIME exclusion list, allowing individual entries to be re-enabled for compression. Within the same list, when multiple tokens match the same MIME type, the last one listed wins, positive or negative (gitignore-style precedence). The built-in list is only consulted as a fallback when the user-configured list produces no definitive match. --- zstd.c | 49 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/zstd.c b/zstd.c index 4386faa..f0891e0 100644 --- a/zstd.c +++ b/zstd.c @@ -1296,13 +1296,17 @@ static int php_zstd_output_encoding(void) return PHP_ZSTD_G(compression_coding); } -static int php_zstd_output_mimetype_excluded(const char *exclude) +/* returns 1 if a positive token matches, -1 if a negative (!) token + * matches, 0 if no token matches; when multiple tokens in the same + * list match, the last one listed wins (gitignore-style precedence) */ +static int php_zstd_output_mimetype_match(const char *list) { const char *mimetype = SG(sapi_headers).mimetype; const char *p, *end; size_t mimetype_len; + int last_match = 0; - if (!mimetype || !*mimetype || !exclude || !*exclude) { + if (!mimetype || !*mimetype || !list || !*list) { return 0; } @@ -1312,10 +1316,11 @@ static int php_zstd_output_mimetype_excluded(const char *exclude) end++; } mimetype_len = end - mimetype; - p = exclude; + p = list; while (*p) { size_t token_len; + zend_bool negated = 0; while (*p == ',' || *p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') { @@ -1330,27 +1335,51 @@ static int php_zstd_output_mimetype_excluded(const char *exclude) token_len = end - p; + if (token_len > 0 && *p == '!') { + negated = 1; + p++; + token_len--; + } + if (token_len > 0) { + zend_bool matched = 0; + if (token_len >= 2 && p[token_len - 2] == '/' && p[token_len - 1] == '*') { size_t prefix_len = token_len - 1; if (mimetype_len >= prefix_len && !strncasecmp(mimetype, p, prefix_len)) { - return 1; + matched = 1; } } - if (mimetype_len == token_len + if (!matched && mimetype_len == token_len && !strncasecmp(mimetype, p, token_len)) { - return 1; + matched = 1; + } + + if (matched) { + last_match = negated ? -1 : 1; } } p = end; } - return 0; + return last_match; +} + +static int php_zstd_output_mimetype_excluded(void) +{ + int result = php_zstd_output_mimetype_match( + PHP_ZSTD_G(output_compression_exclude_types)); + + if (result != 0) { + return result > 0; + } + + return php_zstd_output_mimetype_match(ZSTD_MIMETYPE_EXCLUDE) > 0; } static zend_string* @@ -1558,11 +1587,7 @@ php_zstd_output_handler(void **handler_context, php_zstd_context *ctx = *(php_zstd_context **) handler_context; if ((output_context->op & PHP_OUTPUT_HANDLER_START) - && ( - php_zstd_output_mimetype_excluded(ZSTD_MIMETYPE_EXCLUDE) - || - php_zstd_output_mimetype_excluded(PHP_ZSTD_G(output_compression_exclude_types)) - )) { + && php_zstd_output_mimetype_excluded()) { return FAILURE; } From a1988a26498feeddf6d84e3e3a85fbd66798da4c Mon Sep 17 00:00:00 2001 From: kjdev Date: Thu, 13 Aug 2026 08:33:20 +0900 Subject: [PATCH 2/3] test: add tests for exclude_types negation tokens Cover the default built-in exclusion, overriding it with a negation token, and the last-matching-token-wins precedence when a positive and a negative token in the same list both match, in either order. --- tests/ob_exclude_006.phpt | 19 +++++++++++++++++++ tests/ob_exclude_007.phpt | 23 +++++++++++++++++++++++ tests/ob_exclude_008.phpt | 23 +++++++++++++++++++++++ tests/ob_exclude_009.phpt | 20 ++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 tests/ob_exclude_006.phpt create mode 100644 tests/ob_exclude_007.phpt create mode 100644 tests/ob_exclude_008.phpt create mode 100644 tests/ob_exclude_009.phpt diff --git a/tests/ob_exclude_006.phpt b/tests/ob_exclude_006.phpt new file mode 100644 index 0000000..e980862 --- /dev/null +++ b/tests/ob_exclude_006.phpt @@ -0,0 +1,19 @@ +--TEST-- +zstd.output_compression_exclude_types built-in exclusion applies by default +--SKIPIF-- + +--INI-- +zstd.output_compression=1 +--ENV-- +HTTP_ACCEPT_ENCODING=zstd +--GET-- +ob=025 +--FILE-- + +--EXPECT-- +hi diff --git a/tests/ob_exclude_007.phpt b/tests/ob_exclude_007.phpt new file mode 100644 index 0000000..ee44233 --- /dev/null +++ b/tests/ob_exclude_007.phpt @@ -0,0 +1,23 @@ +--TEST-- +zstd.output_compression_exclude_types negation token overrides built-in exclusion +--SKIPIF-- + +--INI-- +zstd.output_compression=1 +zstd.output_compression_exclude_types="!image/png" +--ENV-- +HTTP_ACCEPT_ENCODING=zstd +--GET-- +ob=026 +--FILE-- + +--EXPECT_EXTERNAL-- +files/ob_001.zstd +--EXPECTHEADERS-- +Content-Encoding: zstd +Vary: Accept-Encoding diff --git a/tests/ob_exclude_008.phpt b/tests/ob_exclude_008.phpt new file mode 100644 index 0000000..995e69a --- /dev/null +++ b/tests/ob_exclude_008.phpt @@ -0,0 +1,23 @@ +--TEST-- +zstd.output_compression_exclude_types later negation token wins over earlier positive match +--SKIPIF-- + +--INI-- +zstd.output_compression=1 +zstd.output_compression_exclude_types="image/*,!image/png" +--ENV-- +HTTP_ACCEPT_ENCODING=zstd +--GET-- +ob=027 +--FILE-- + +--EXPECT_EXTERNAL-- +files/ob_001.zstd +--EXPECTHEADERS-- +Content-Encoding: zstd +Vary: Accept-Encoding diff --git a/tests/ob_exclude_009.phpt b/tests/ob_exclude_009.phpt new file mode 100644 index 0000000..6971fc9 --- /dev/null +++ b/tests/ob_exclude_009.phpt @@ -0,0 +1,20 @@ +--TEST-- +zstd.output_compression_exclude_types later positive token wins over earlier negation +--SKIPIF-- + +--INI-- +zstd.output_compression=1 +zstd.output_compression_exclude_types="!image/png,image/*" +--ENV-- +HTTP_ACCEPT_ENCODING=zstd +--GET-- +ob=028 +--FILE-- + +--EXPECT-- +hi From b2b4ce6166cc39231a71f8bba3db2e14cebe5f45 Mon Sep 17 00:00:00 2001 From: kjdev Date: Thu, 13 Aug 2026 08:33:37 +0900 Subject: [PATCH 3/3] docs: document negation tokens for exclude_types Explain the `!` token for re-enabling individual entries from the built-in exclusion list, and the last-match-wins precedence rule (gitignore-style: later tokens in the same list override earlier ones, whether positive or negative). --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 9dbfe56..176f4b5 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,33 @@ zstd.output\_compression\_dict | "" | PHP\_INI\_ALL This is useful for already-compressed binary formats where additional Zstd compression usually provides little benefit. + A token prefixed with `!` negates a match: it makes the extension + compress that MIME type even if it is in the built-in exclusion list. + Negation also supports exact matches and `type/*` wildcards. Since + `!` is a special character in php.ini, values that use it must be + quoted. + + ```ini + zstd.output_compression_exclude_types="!image/png" + ``` + + Within the same list, if a MIME type matches multiple tokens, the + last matching token wins, regardless of whether it is positive or + negative (the same precedence rule `.gitignore` uses). This allows + patterns such as: + + ```ini + zstd.output_compression_exclude_types="image/*,!image/png" + ``` + + which excludes all `image/*` types except `image/png`, since the + negation is listed after the wildcard. + + Negation only applies within this setting: if it doesn't produce a + definitive match for the current MIME type, the built-in list is + still consulted. The built-in list's contents can be seen in the + phpinfo() output. + * zstd.output\_compression\_dict _string_ Specifies the path to the compressed dictionary file to be