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 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 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; }