Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ext/filter/filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ static const filter_list_entry filter_list[] = {
{ "validate_email", FILTER_VALIDATE_EMAIL, php_filter_validate_email },
{ "validate_ip", FILTER_VALIDATE_IP, php_filter_validate_ip },
{ "validate_mac", FILTER_VALIDATE_MAC, php_filter_validate_mac },
{ "validate_strlen", FILTER_VALIDATE_STRLEN, php_filter_validate_strlen },

{ "string", FILTER_SANITIZE_STRING, php_filter_string },
{ "stripped", FILTER_SANITIZE_STRING, php_filter_string },
Expand Down
5 changes: 5 additions & 0 deletions ext/filter/filter.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@
* @cvalue FILTER_VALIDATE_MAC
*/
const FILTER_VALIDATE_MAC = UNKNOWN;
/**
* @var int
* @cvalue FILTER_VALIDATE_STRLEN
*/
const FILTER_VALIDATE_STRLEN = UNKNOWN;

/**
* @var int
Expand Down
3 changes: 2 additions & 1 deletion ext/filter/filter_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion ext/filter/filter_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
#define FILTER_VALIDATE_IP 0x0113
#define FILTER_VALIDATE_MAC 0x0114
#define FILTER_VALIDATE_DOMAIN 0x0115
#define FILTER_VALIDATE_LAST 0x0115
#define FILTER_VALIDATE_STRLEN 0x0116
#define FILTER_VALIDATE_LAST 0x0116

#define FILTER_VALIDATE_ALL 0x0100

Expand Down
69 changes: 69 additions & 0 deletions ext/filter/logical_filters.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "filter_private.h"
#include "ext/pcre/php_pcre.h"
#include "ext/uri/php_uri.h"
#include "ext/standard/html.h"

#include "zend_multiply.h"

Expand All @@ -32,6 +33,14 @@
# define INADDR_NONE ((unsigned long int) -1)
#endif

#ifdef HAVE_ARPA_INET_H
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: look like a copy/paste mistake (same for INADDR_NONE) ?
already defined just above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review.

# include <arpa/inet.h>
#endif

#ifndef INADDR_NONE
# define INADDR_NONE ((unsigned long int) -1)
#endif


/* {{{ FETCH_DOUBLE_OPTION(var_name, option_name) */
#define FETCH_DOUBLE_OPTION(var_name, option_name) \
Expand Down Expand Up @@ -1115,3 +1124,63 @@ zend_result php_filter_validate_mac(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */
return SUCCESS;
}
/* }}} */

/**
* Returns the number of Unicode code points in a UTF-8 encoded string.
* Invalid UTF-8 byte sequences (U+FFFD) are counted as one replacement character.
*/
static size_t php_utf8_strlen(const unsigned char *str, size_t str_len)
{
size_t len = 0, cursor = 0;
zend_result status;

while (cursor < str_len) {
php_next_utf8_char(str, str_len, &cursor, &status);
len++;
}

return len;
}


zend_result php_filter_validate_strlen(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */
{
int min_len_set, max_len_set;
zval *option_val;
zend_long min_len, max_len;
size_t len;
const char *str = Z_STRVAL_P(value);
size_t str_size = Z_STRLEN_P(value);

FETCH_LONG_OPTION(min_len, "min_len");
FETCH_LONG_OPTION(max_len, "max_len");

if (min_len_set && min_len < 0) {
php_error_docref(NULL, E_WARNING,
"min_len must be greater than or equal to 0");
RETURN_VALIDATION_FAILED;
}

if (max_len_set && max_len < 0) {
php_error_docref(NULL, E_WARNING,
"max_len must be greater than or equal to 0");
RETURN_VALIDATION_FAILED;
}

if (min_len_set && max_len_set && min_len > max_len) {
php_error_docref(NULL, E_WARNING,
"min_len must be less than or equal to max_len");
RETURN_VALIDATION_FAILED;
}

len = php_utf8_strlen((const unsigned char *)str, str_size);

if (min_len_set && len < min_len) {
RETURN_VALIDATION_FAILED;
}
if (max_len_set && max_len < len) {
RETURN_VALIDATION_FAILED;
}
return SUCCESS;
}
/* }}} */
1 change: 1 addition & 0 deletions ext/filter/php_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ zend_result php_filter_validate_url(PHP_INPUT_FILTER_PARAM_DECL);
zend_result php_filter_validate_email(PHP_INPUT_FILTER_PARAM_DECL);
zend_result php_filter_validate_ip(PHP_INPUT_FILTER_PARAM_DECL);
zend_result php_filter_validate_mac(PHP_INPUT_FILTER_PARAM_DECL);
zend_result php_filter_validate_strlen(PHP_INPUT_FILTER_PARAM_DECL);

zend_result php_filter_string(PHP_INPUT_FILTER_PARAM_DECL);
zend_result php_filter_encoded(PHP_INPUT_FILTER_PARAM_DECL);
Expand Down
26 changes: 26 additions & 0 deletions ext/filter/tests/validate_str_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
FILTER_VALIDATE_STR: Emoji string length
--SKIPIF--
<?php if (!extension_loaded("filter")) die("skip"); ?>
--FILE--
<?php
$options1 = ['options' => ['min_len' => 2, 'max_len' => 2]];
$options2 = ['options' => ['max_len' => 2, 'default' => 'error']];
$options3 = ['options' => ['min_len' => 0]];

var_dump(
filter_var('ab', filter_id('validate_strlen'), $options1),
filter_var('🐘🐘', FILTER_VALIDATE_STRLEN, $options1),
filter_var('🐘', FILTER_VALIDATE_STRLEN, $options1),
filter_var('🐘🐘🐘', FILTER_VALIDATE_STRLEN, $options1),
filter_var('🐘🐘🐘', FILTER_VALIDATE_STRLEN, $options2),
filter_var('', FILTER_VALIDATE_STRLEN, $options3),
);
?>
--EXPECT--
string(2) "ab"
string(8) "🐘🐘"
bool(false)
bool(false)
string(5) "error"
string(0) ""
37 changes: 37 additions & 0 deletions ext/filter/tests/validate_str_invalid_options.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
FILTER_VALIDATE_STR: invalid min_len/max_len options
--FILE--
<?php

echo "--- min_len negative ---\n";
var_dump(filter_var("abc", FILTER_VALIDATE_STRLEN, [
"options" => ["min_len" => -1]
]));

echo "--- max_len negative ---\n";
var_dump(filter_var("abc", FILTER_VALIDATE_STRLEN, [
"options" => ["max_len" => -1]
]));

echo "--- min_len greater than max_len ---\n";
var_dump(filter_var("abc", FILTER_VALIDATE_STRLEN, [
"options" => [
"min_len" => 10,
"max_len" => 5
]
]));

?>
--EXPECTF--
--- min_len negative ---

Warning: filter_var(): min_len must be greater than or equal to 0 in %s on line %d
bool(false)
--- max_len negative ---

Warning: filter_var(): max_len must be greater than or equal to 0 in %s on line %d
bool(false)
--- min_len greater than max_len ---

Warning: filter_var(): min_len must be less than or equal to max_len in %s on line %d
bool(false)
43 changes: 43 additions & 0 deletions ext/filter/tests/validate_str_invalid_type.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
FILTER_VALIDATE_STR: Invalid input types
--SKIPIF--
<?php if (!extension_loaded("filter")) die("skip"); ?>
--FILE--
<?php

/**
* According to the filter_var() manual:
* https://www.php.net/manual/en/function.filter-var.php
* - Scalar types (int, float, bool) are automatically converted to strings before filtering.
* - Non-scalar types (array, object, resource, null) always result in false.
*
* This test ensures FILTER_VALIDATE_STRLEN behaves accordingly for various input types.
*/

$options = ['options' => ['min_len' => 2, 'max_len' => 4]];
$handle = fopen("php://memory", "r");
class Dummy { public $x = 1; }

var_dump(
filter_var(1234, FILTER_VALIDATE_STRLEN, $options),
filter_var(3.14, FILTER_VALIDATE_STRLEN, $options),
filter_var(['a', 'b'], FILTER_VALIDATE_STRLEN, $options),
filter_var(new Dummy(), FILTER_VALIDATE_STRLEN, $options),
filter_var(NULL, FILTER_VALIDATE_STRLEN, $options),
filter_var(true, FILTER_VALIDATE_STRLEN, $options),
filter_var(false, FILTER_VALIDATE_STRLEN, $options),
filter_var($handle, FILTER_VALIDATE_STRLEN, $options)
);

fclose($handle);

?>
--EXPECT--
string(4) "1234"
string(4) "3.14"
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
35 changes: 35 additions & 0 deletions ext/filter/tests/validate_str_unicode.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--TEST--
FILTER_VALIDATE_STR: Unicode maximal subpart validation (Table 3-11 reference)
--SKIPIF--
<?php if (!extension_loaded("filter")) die("skip"); ?>
--FILE--
<?php
/**
* These tests verify that FILTER_VALIDATE_STR correctly counts invalid or truncated UTF-8 sequences
* as single characters, per Unicode Standard Section 3.9.6: "U+FFFD Substitution of Maximal Subparts"
* (https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G66453).
*
* The filter only performs validation and does not modify the original data.
* As long as the length constraint is satisfied, the input byte string is returned.
*
* Example 1 (single invalid byte):
* Input: 61 80 ("a" followed by invalid \x80)
* Validation: 2 code points (min_range = 2, max_range = 2) → returns original input
* Expected hex: 6180
*
* Example 2 (Table 3-11 inspired):
* Input: E1 80 E2 F0 91 92 F1 BF 41
* Validation: 5 code points (max_range = 5) → returns original input
* Expected hex: e180e2f09192f1bf41
*
*/

$options1 = ['options' => ['min_range' => 2, 'max_range' => 2]];
$options2 = ['options' => ['max_range' => 5]];

echo bin2hex(filter_var("a\x80", FILTER_VALIDATE_STRLEN, $options1)), "\n";
echo bin2hex(filter_var("\xE1\x80\xE2\xF0\x91\x92\xF1\xBF\x41", FILTER_VALIDATE_STRLEN, $options2)), "\n";
?>
--EXPECT--
6180
e180e2f09192f1bf41
Loading