From 0dbf5d78455d4d6a41d186da50adc1122ec066f4 Mon Sep 17 00:00:00 2001 From: vajexal <72415539+vajexal@users.noreply.github.com> Date: Tue, 29 Dec 2020 17:50:06 +0300 Subject: [PATCH] Fix labels length check (#278) * Fix label length check * Test label too long on single word hostname --- src/Parser/DomainPart.php | 36 +++++++++++++++---- src/Warning/Warning.php | 2 +- .../NoRFCWarningsValidationTest.php | 15 ++++++-- .../Validation/RFCValidationTest.php | 21 ++++++++--- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/Parser/DomainPart.php b/src/Parser/DomainPart.php index 28a0510..4dadba8 100644 --- a/src/Parser/DomainPart.php +++ b/src/Parser/DomainPart.php @@ -35,6 +35,7 @@ use Egulias\EmailValidator\Warning\TLD; class DomainPart extends Parser { const DOMAIN_MAX_LENGTH = 254; + const LABEL_MAX_LENGTH = 63; /** * @var string @@ -160,6 +161,7 @@ class DomainPart extends Parser protected function doParseDomainPart() { $domain = ''; + $label = ''; $openedParenthesis = 0; do { $prev = $this->lexer->getPrevious(); @@ -190,7 +192,12 @@ class DomainPart extends Parser $this->parseDomainLiteral(); } - $this->checkLabelLength($prev); + if ($this->lexer->token['type'] === EmailLexer::S_DOT) { + $this->checkLabelLength($label); + $label = ''; + } else { + $label .= $this->lexer->token['value']; + } if ($this->isFWS()) { $this->parseFWS(); @@ -203,6 +210,8 @@ class DomainPart extends Parser } } while (null !== $this->lexer->token['type']); + $this->checkLabelLength($label); + return $domain; } @@ -386,16 +395,31 @@ class DomainPart extends Parser return true; } - protected function checkLabelLength(array $prev) + /** + * @param string $label + */ + protected function checkLabelLength($label) { - if ($this->lexer->token['type'] === EmailLexer::S_DOT && - $prev['type'] === EmailLexer::GENERIC && - strlen($prev['value']) > 63 - ) { + if ($this->isLabelTooLong($label)) { $this->warnings[LabelTooLong::CODE] = new LabelTooLong(); } } + /** + * @param string $label + * @return bool + */ + private function isLabelTooLong($label) + { + if (preg_match('/[^\x00-\x7F]/', $label)) { + idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo); + + return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG); + } + + return strlen($label) > self::LABEL_MAX_LENGTH; + } + protected function parseDomainComments() { $this->isUnclosedComment(); diff --git a/src/Warning/Warning.php b/src/Warning/Warning.php index bce7e7a..a2ee7b0 100644 --- a/src/Warning/Warning.php +++ b/src/Warning/Warning.php @@ -29,7 +29,7 @@ abstract class Warning */ public function code() { - return self::CODE; + return static::CODE; } /** diff --git a/tests/EmailValidator/Validation/NoRFCWarningsValidationTest.php b/tests/EmailValidator/Validation/NoRFCWarningsValidationTest.php index ce335c1..6e972a4 100644 --- a/tests/EmailValidator/Validation/NoRFCWarningsValidationTest.php +++ b/tests/EmailValidator/Validation/NoRFCWarningsValidationTest.php @@ -26,11 +26,22 @@ class NoRFCWarningsValidationTest extends TestCase $this->assertInstanceOf(RFCWarnings::class, $validation->getError()); } - public function testEmailWithoutWarningsIsValid() + /** + * @dataProvider getValidEmailsWithoutWarnings + */ + public function testEmailWithoutWarningsIsValid($email) { $validation = new NoRFCWarningsValidation(); - $this->assertTrue($validation->isValid('example@example.com', new EmailLexer())); + $this->assertTrue($validation->isValid($email, new EmailLexer())); $this->assertNull($validation->getError()); } + + public function getValidEmailsWithoutWarnings() + { + return [ + ['example@example.com',], + [sprintf('example@%s.com', str_repeat('ъ', 40)),], + ]; + } } diff --git a/tests/EmailValidator/Validation/RFCValidationTest.php b/tests/EmailValidator/Validation/RFCValidationTest.php index f1a649f..d975f58 100644 --- a/tests/EmailValidator/Validation/RFCValidationTest.php +++ b/tests/EmailValidator/Validation/RFCValidationTest.php @@ -24,6 +24,7 @@ use Egulias\EmailValidator\Warning\CFWSWithFWS; use Egulias\EmailValidator\Warning\Comment; use Egulias\EmailValidator\Warning\DomainLiteral; use Egulias\EmailValidator\Warning\DomainTooLong; +use Egulias\EmailValidator\Warning\EmailTooLong; use Egulias\EmailValidator\Warning\IPV6BadChar; use Egulias\EmailValidator\Warning\IPV6ColonEnd; use Egulias\EmailValidator\Warning\IPV6ColonStart; @@ -226,12 +227,12 @@ class RFCValidationTest extends TestCase $this->assertTrue($this->validator->isValid($email, $this->lexer)); $warnings = $this->validator->getWarnings(); $this->assertCount( - count($warnings), $expectedWarnings, + count($expectedWarnings), $warnings, "Expected: " . implode(",", $expectedWarnings) . " and got " . implode(",", $warnings) ); foreach ($warnings as $warning) { - $this->assertArrayHasKey($warning->code(), $expectedWarnings); + $this->assertContains($warning->code(), $expectedWarnings); } } @@ -278,17 +279,29 @@ class RFCValidationTest extends TestCase 'example@toolonglocalparttoolonglocalparttoolonglocalparttoolonglocalpart.co.uk' ], [ - [DomainTooLong::CODE, LabelTooLong::CODE,], + [DomainTooLong::CODE, LabelTooLong::CODE, EmailTooLong::CODE], 'example2@toolonglocalparttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocal'. 'parttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocalpart'. 'toolonglocalparttoolonglocalparttoolonglocalparttoolonglocalpart' ], [ - [DomainTooLong::CODE, LabelTooLong::CODE,], + [DomainTooLong::CODE, LabelTooLong::CODE, EmailTooLong::CODE], 'example@toolonglocalparttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocal'. 'parttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocalparttoolonglocalpart'. 'toolonglocalparttoolonglocalparttoolonglocalparttoolonglocalpar' ], + [ + [LabelTooLong::CODE,], + sprintf('example@%s.com', str_repeat('ъ', 60)), + ], + [ + [LabelTooLong::CODE,], + sprintf('example@%s.com', str_repeat('a4t', 22)), + ], + [ + [LabelTooLong::CODE,], + sprintf('example@%s', str_repeat('a4t', 22)), + ], ]; } }