Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.

# 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