Skip to content

Commit

Permalink
Merge pull request #76 from vanilla/backport/1.x/maxByteLength-property
Browse files Browse the repository at this point in the history
[Backport 1.x] Allow validating maxByteLength separately from maxLength
  • Loading branch information
tburry committed Jan 6, 2022
2 parents 2f77898 + a64baad commit d00dc05
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 16 deletions.
29 changes: 23 additions & 6 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class Schema implements \JsonSerializable, \ArrayAccess {
*/
public function __construct($schema = []) {
$this->schema = $schema;
$this->setFlag(self::VALIDATE_STRING_LENGTH_AS_UNICODE, true);
}

/**
Expand Down Expand Up @@ -1010,10 +1011,8 @@ protected function validateString($value, ValidationField $field) {
return Invalid::value();
}

$errorCount = $field->getErrorCount();
$strFn = $this->hasFlag(self::VALIDATE_STRING_LENGTH_AS_UNICODE) ? "mb_strlen" : "strlen";
$strLen = $strFn($value);
if (($minLength = $field->val('minLength', 0)) > 0 && $strLen < $minLength) {
$mbStrLen = mb_strlen($value);
if (($minLength = $field->val('minLength', 0)) > 0 && $mbStrLen < $minLength) {
if (!empty($field->getName()) && $minLength === 1) {
$field->addError('missingField', ['messageCode' => '{field} is required.', 'status' => 422]);
} else {
Expand All @@ -1027,17 +1026,35 @@ protected function validateString($value, ValidationField $field) {
);
}
}
if (($maxLength = $field->val('maxLength', 0)) > 0 && $strLen > $maxLength) {
if (($maxLength = $field->val('maxLength', 0)) > 0 && $mbStrLen > $maxLength) {
$field->addError(
'maxLength',
[
'messageCode' => '{field} is {overflow} {overflow,plural,characters} too long.',
'maxLength' => $maxLength,
'overflow' => $strLen - $maxLength,
'overflow' => $mbStrLen - $maxLength,
'status' => 422
]
);
}

$useLengthAsByteLength = !$this->hasFlag(self::VALIDATE_STRING_LENGTH_AS_UNICODE);
$maxByteLength = $field->val('maxByteLength', $useLengthAsByteLength ? $maxLength : null);
if ($maxByteLength !== null && $maxByteLength > 0) {
$byteStrLen = strlen($value);
if ($byteStrLen > $maxByteLength) {
$field->addError(
'maxByteLength',
[
'messageCode' => '{field} is {overflow} {overflow,plural,byte,bytes} too long.',
'maxLength' => $maxLength,
'overflow' => $byteStrLen - $maxByteLength,
'status' => 422,
]
);
}
}

if ($pattern = $field->val('pattern')) {
$regex = '`'.str_replace('`', preg_quote('`', '`'), $pattern).'`';

Expand Down
1 change: 1 addition & 0 deletions tests/PropertyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function testPropertyAccess() {
$this->assertSame('foo', $schema->jsonSerialize()['description']);
$this->assertSame('foo', $schema['description']);

$schema->setFlag(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE, false);
$this->assertSame(0, $schema->getFlags());
$behaviors = [
Schema::VALIDATE_EXTRA_PROPERTY_NOTICE,
Expand Down
77 changes: 67 additions & 10 deletions tests/StringValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function provideMinLengthTests() {
'abcd' => ['abcd', ''],
'empty 1' => ['', 'missingField', 1],
'empty 0' => ['', '', 0],
'unicode as bytes success' => ['😱', '', 4],
'unicode as bytes success' => ['😱', 'minLength', 4],
'unicode as unicode fail' => ['😱', 'minLength', 2, Schema::VALIDATE_STRING_LENGTH_AS_UNICODE],
'unicode as unicode success' => ['😱', '', 1, Schema::VALIDATE_STRING_LENGTH_AS_UNICODE],
];
Expand All @@ -73,19 +73,14 @@ public function provideMinLengthTests() {
* @param string $str The string to test.
* @param string $code The expected error code, if any.
* @param int $maxLength The max length to test.
* @param int $flags Flags to set on the schema.
*
* @dataProvider provideMaxLengthTests
*/
public function testMaxLength($str, string $code = '', int $maxLength = 3, int $flags = null) {
public function testMaxLength($str, string $code = '', int $maxLength = 3) {
$schema = Schema::parse(['str:s?' => [
'maxLength' => $maxLength,
]]);

if ($flags !== null) {
$schema->setFlags($flags);
}

try {
$schema->validate(['str' => $str]);

Expand All @@ -111,14 +106,76 @@ public function provideMaxLengthTests() {
'ab' => ['ab'],
'abc' => ['abc'],
'abcd' => ['abcd', 'maxLength'],
'long multibyte with unicode length' => ['😱', '', 2, Schema::VALIDATE_STRING_LENGTH_AS_UNICODE],
'long multibyte with byte length' => ['😱', 'maxLength', 2],
'exact amount multibyte with byte length' => ['😱', '', 4],
];

return $r;
}

/**
* Test byte length validation.
*
* @param array $value
* @param string|array|null $exceptionMessages Null, an expected exception message, or multiple expected exception messages.
* @param bool $forceByteLength Set this to true to force all maxLengths to be byte length.
*
* @dataProvider provideByteLengths
*/
public function testByteLengthValidation(array $value, $exceptionMessages = null, bool $forceByteLength = false) {
$schema = Schema::parse([
'justLength:s?' => [
'maxLength' => 4,
],
'justByteLength:s?' => [
'maxByteLength' => 8,
],
'mixedLengths:s?' => [
'maxLength' => 4,
'maxByteLength' => 6
],
]);
if ($forceByteLength) {
$schema->setFlag(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE, false);
}

try {
$schema->validate($value);
// We were expecting success.
$this->assertTrue(true);
} catch (ValidationException $e) {
if ($exceptionMessages !== null) {
$actual = $e->getMessage();
$exceptionMessages = is_array($exceptionMessages) ? $exceptionMessages : [$exceptionMessages];
foreach ($exceptionMessages as $expected) {
$this->assertContains($expected, $actual);
}
} else {
throw $e;
}
}
}

/**
* @return array
*/
public function provideByteLengths() {
return [
'maxLength - short' => [['justLength' => '😱']],
'maxLength - equal' => [['justLength' => '😱😱😱😱']],
'maxLength - long' => [['justLength' => '😱😱😱😱😱'], '1 characters too long'],
'byteLength - short' => [['justByteLength' => '😱']],
'byteLength - equal' => [['justByteLength' => '😱😱']],
'byteLength - long' => [['justByteLength' => '😱😱a'], '1 byte too long'],
'mixedLengths - short' => [['mixedLengths' => '😱']],
'mixedLengths - equal' => [['mixedLengths' => '😱aa']],
'mixedLengths - long bytes' => [['mixedLengths' => '😱😱'], '2 bytes too long'],
'mixedLengths - long chars' => [['mixedLengths' => 'aaaaa'], '1 characters too long'],
'mixedLengths - long chars - long bytes' => [['mixedLengths' => '😱😱😱😱😱'], ["1 characters too long", "14 bytes too long."]],
'byteLength flag - short' => [['justLength' => '😱'], null, true],
'byteLength flag - long' => [['justLength' => '😱😱😱😱'], '12 bytes too long', true],
'byteLength property is preferred over byte length flag' => [['mixedLengths' => '😱😱'], '2 bytes too long', true]
];
}

/**
* Test string pattern constraints.
*
Expand Down

0 comments on commit d00dc05

Please sign in to comment.