From 16b9210bca845fa185c08381a10b3746f9deae6f Mon Sep 17 00:00:00 2001 From: Ninos Ego Date: Wed, 3 Apr 2024 14:03:51 +0200 Subject: [PATCH] [Validator] Add support for types (`ALL*`, `LOCAL_*`, `UNIVERSAL_*`, `UNICAST_*`, `MULTICAST_*`, `BROADCAST`) in `MacAddress` constraint --- .../Validator/Constraints/MacAddress.php | 41 ++ .../Constraints/MacAddressValidator.php | 67 ++- .../Tests/Constraints/MacAddressTest.php | 14 +- .../Constraints/MacAddressValidatorTest.php | 420 ++++++++++++++++++ 4 files changed, 538 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/MacAddress.php b/src/Symfony/Component/Validator/Constraints/MacAddress.php index 7f1f6325465c7..d0062bc56abfc 100644 --- a/src/Symfony/Component/Validator/Constraints/MacAddress.php +++ b/src/Symfony/Component/Validator/Constraints/MacAddress.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Validates that a value is a valid MAC address. @@ -21,22 +22,62 @@ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class MacAddress extends Constraint { + public const ALL = 'all'; + public const ALL_NO_BROADCAST = 'all_no_broadcast'; + public const LOCAL_ALL = 'local_all'; + public const LOCAL_NO_BROADCAST = 'local_no_broadcast'; + public const LOCAL_UNICAST = 'local_unicast'; + public const LOCAL_MULTICAST = 'local_multicast'; + public const LOCAL_MULTICAST_NO_BROADCAST = 'local_multicast_no_broadcast'; + public const UNIVERSAL_ALL = 'universal_all'; + public const UNIVERSAL_UNICAST = 'universal_unicast'; + public const UNIVERSAL_MULTICAST = 'universal_multicast'; + public const UNICAST_ALL = 'unicast_all'; + public const MULTICAST_ALL = 'multicast_all'; + public const MULTICAST_NO_BROADCAST = 'multicast_no_broadcast'; + public const BROADCAST = 'broadcast'; + public const INVALID_MAC_ERROR = 'a183fbff-6968-43b4-82a2-cc5cf7150036'; + private const TYPES = [ + self::ALL, + self::ALL_NO_BROADCAST, + self::LOCAL_ALL, + self::LOCAL_NO_BROADCAST, + self::LOCAL_UNICAST, + self::LOCAL_MULTICAST, + self::LOCAL_MULTICAST_NO_BROADCAST, + self::UNIVERSAL_ALL, + self::UNIVERSAL_UNICAST, + self::UNIVERSAL_MULTICAST, + self::UNICAST_ALL, + self::MULTICAST_ALL, + self::MULTICAST_NO_BROADCAST, + self::BROADCAST, + ]; + protected const ERROR_NAMES = [ self::INVALID_MAC_ERROR => 'INVALID_MAC_ERROR', ]; public ?\Closure $normalizer; + /** + * @param self::ALL*|self::LOCAL_*|self::UNIVERSAL_*|self::UNICAST_*|self::MULTICAST_*|self::BROADCAST $type A mac address type to validate (defaults to {@see self::ALL}) + */ public function __construct( public string $message = 'This value is not a valid MAC address.', + public string $type = self::ALL, ?callable $normalizer = null, ?array $groups = null, mixed $payload = null, ) { parent::__construct(null, $groups, $payload); + if (!\in_array($this->type, self::TYPES, true)) { + throw new ConstraintDefinitionException(sprintf('The option "type" must be one of "%s".', implode('", "', self::TYPES))); + } + $this->normalizer = null !== $normalizer ? $normalizer(...) : null; } } diff --git a/src/Symfony/Component/Validator/Constraints/MacAddressValidator.php b/src/Symfony/Component/Validator/Constraints/MacAddressValidator.php index a93f90b4d877f..80379812816cc 100644 --- a/src/Symfony/Component/Validator/Constraints/MacAddressValidator.php +++ b/src/Symfony/Component/Validator/Constraints/MacAddressValidator.php @@ -43,11 +43,76 @@ public function validate(mixed $value, Constraint $constraint): void $value = ($constraint->normalizer)($value); } - if (!filter_var($value, \FILTER_VALIDATE_MAC)) { + if (!self::checkMac($value, $constraint->type)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(MacAddress::INVALID_MAC_ERROR) ->addViolation(); } } + + /** + * Checks whether a MAC address is valid. + */ + private static function checkMac(string $mac, string $type): bool + { + if (!filter_var($mac, \FILTER_VALIDATE_MAC)) { + return false; + } + + return match ($type) { + MacAddress::ALL => true, + MacAddress::ALL_NO_BROADCAST => !self::isBroadcast($mac), + MacAddress::LOCAL_ALL => self::isLocal($mac), + MacAddress::LOCAL_NO_BROADCAST => self::isLocal($mac) && !self::isBroadcast($mac), + MacAddress::LOCAL_UNICAST => self::isLocal($mac) && self::isUnicast($mac), + MacAddress::LOCAL_MULTICAST => self::isLocal($mac) && !self::isUnicast($mac), + MacAddress::LOCAL_MULTICAST_NO_BROADCAST => self::isLocal($mac) && !self::isUnicast($mac) && !self::isBroadcast($mac), + MacAddress::UNIVERSAL_ALL => !self::isLocal($mac), + MacAddress::UNIVERSAL_UNICAST => !self::isLocal($mac) && self::isUnicast($mac), + MacAddress::UNIVERSAL_MULTICAST => !self::isLocal($mac) && !self::isUnicast($mac), + MacAddress::UNICAST_ALL => self::isUnicast($mac), + MacAddress::MULTICAST_ALL => !self::isUnicast($mac), + MacAddress::MULTICAST_NO_BROADCAST => !self::isUnicast($mac) && !self::isBroadcast($mac), + MacAddress::BROADCAST => self::isBroadcast($mac), + }; + } + + /** + * Checks whether a MAC address is unicast or multicast. + */ + private static function isUnicast(string $mac): bool + { + return match (self::sanitize($mac)[1]) { + '0', '4', '8', 'c', '2', '6', 'a', 'e' => true, + default => false, + }; + } + + /** + * Checks whether a MAC address is local or universal. + */ + private static function isLocal(string $mac): bool + { + return match (self::sanitize($mac)[1]) { + '2', '6', 'a', 'e', '3', '7', 'b', 'f' => true, + default => false, + }; + } + + /** + * Checks whether a MAC address is broadcast. + */ + private static function isBroadcast(string $mac): bool + { + return 'ffffffffffff' === self::sanitize($mac); + } + + /** + * Returns the sanitized MAC address. + */ + private static function sanitize(string $mac): string + { + return strtolower(str_replace([':', '-', '.'], '', $mac)); + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressTest.php b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressTest.php index 0669f3ed8f87d..94cbfedd4747f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressTest.php @@ -37,11 +37,16 @@ public function testAttributes() [$aConstraint] = $metadata->properties['a']->getConstraints(); self::assertSame('myMessage', $aConstraint->message); self::assertEquals(trim(...), $aConstraint->normalizer); + self::assertSame(MacAddress::ALL, $aConstraint->type); self::assertSame(['Default', 'MacAddressDummy'], $aConstraint->groups); [$bConstraint] = $metadata->properties['b']->getConstraints(); - self::assertSame(['my_group'], $bConstraint->groups); - self::assertSame('some attached data', $bConstraint->payload); + self::assertSame(MacAddress::LOCAL_UNICAST, $bConstraint->type); + self::assertSame(['Default', 'MacAddressDummy'], $bConstraint->groups); + + [$cConstraint] = $metadata->properties['c']->getConstraints(); + self::assertSame(['my_group'], $cConstraint->groups); + self::assertSame('some attached data', $cConstraint->payload); } } @@ -50,6 +55,9 @@ class MacAddressDummy #[MacAddress(message: 'myMessage', normalizer: 'trim')] private $a; - #[MacAddress(groups: ['my_group'], payload: 'some attached data')] + #[MacAddress(type: MacAddress::LOCAL_UNICAST)] private $b; + + #[MacAddress(groups: ['my_group'], payload: 'some attached data')] + private $c; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php index 2552d4bc9ba86..d755df486e140 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/MacAddressValidatorTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraints\MacAddress; use Symfony\Component\Validator\Constraints\MacAddressValidator; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; @@ -46,6 +47,12 @@ public function testExpectsStringCompatibleType() $this->validator->validate(new \stdClass(), new MacAddress()); } + public function testInvalidValidatorType() + { + $this->expectException(ConstraintDefinitionException::class); + new MacAddress(type: 666); + } + /** * @dataProvider getValidMacs */ @@ -65,9 +72,422 @@ public static function getValidMacs(): array ['ff-ff-ff-ff-ff-ff'], ['FF:FF:FF:FF:FF:FF'], ['FF-FF-FF-FF-FF-FF'], + ['FFFF.FFFF.FFFF'], + ]; + } + + public static function getValidLocalUnicastMacs(): array + { + return [ + ['02:00:00:00:00:00'], + ['16-00-00-00-00-00'], + ['2a-00-00-00-00-00'], + ['3e-00-00-00-00-00'], + ['3E00.0000.0000'], + ]; + } + + public static function getValidLocalMulticastMacs(): array + { + return [ + ['03:00:00:00:00:00'], + ['17-00-00-00-00-00'], + ['2b-00-00-00-00-00'], + ['3f-00-00-00-00-00'], + ['3F00.0000.0000'], ]; } + public static function getValidUniversalUnicastMacs(): array + { + return [ + ['00:00:00:00:00:00'], + ['14-00-00-00-00-00'], + ['28-00-00-00-00-00'], + ['3c-00-00-00-00-00'], + ['3C00.0000.0000'], + ]; + } + + public static function getValidUniversalMulticastMacs(): array + { + return [ + ['01:00:00:00:00:00'], + ['15-00-00-00-00-00'], + ['29-00-00-00-00-00'], + ['3d-00-00-00-00-00'], + ['3D00.0000.0000'], + ]; + } + + public static function getValidBroadcastMacs(): array + { + return [ + ['ff:ff:ff:ff:ff:ff'], + ['FF-ff-FF-ff-FF-ff'], + ['fFff.ffff.fffF'], + ]; + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testValidAllNoBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::ALL_NO_BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidBroadcastMacs + */ + public function testInvalidAllNoBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::ALL_NO_BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testValidLocalMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_ALL)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidLocalMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_ALL); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + */ + public function testValidLocalNoBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_NO_BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testInvalidLocalNoBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_NO_BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + */ + public function testValidLocalUnicastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_UNICAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidLocalUnicastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_UNICAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testValidLocalMulticastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_MULTICAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidLocalMulticastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_MULTICAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + */ + public function testValidLocalMulticastNoBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_MULTICAST_NO_BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testInvalidLocalMulticastNoBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_MULTICAST_NO_BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testValidUniversalMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::UNIVERSAL_ALL)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + */ + public function testInvalidUniversalMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::UNIVERSAL_ALL); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidUniversalUnicastMacs + */ + public function testValidUniversalUnicastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::UNIVERSAL_UNICAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidUniversalUnicastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::UNIVERSAL_UNICAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidUniversalMulticastMacs + */ + public function testValidUniversalMulticastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::UNIVERSAL_MULTICAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalUnicastMacs + */ + public function testInvalidUniversalMulticastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::UNIVERSAL_MULTICAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + */ + public function testUnicastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::UNICAST_ALL)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidUnicastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::UNICAST_ALL); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testMulticastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::MULTICAST_ALL)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + */ + public function testInvalidMulticastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::MULTICAST_ALL); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testMulticastNoBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::MULTICAST_NO_BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testInvalidMulticastNoBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::MULTICAST_NO_BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidBroadcastMacs + */ + public function testBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + /** * @dataProvider getValidMacsWithWhitespaces */