Skip to content

Commit

Permalink
feature #54473 [Validator] Add support for types (ALL*, LOCAL_*, …
Browse files Browse the repository at this point in the history
…`UNIVERSAL_*`, `UNICAST_*`, `MULTICAST_*`, `BROADCAST`) in `MacAddress` constraint (Ninos)

This PR was squashed before being merged into the 7.1 branch.

Discussion
----------

[Validator] Add support for types (`ALL*`, `LOCAL_*`, `UNIVERSAL_*`, `UNICAST_*`, `MULTICAST_*`, `BROADCAST`) in `MacAddress` constraint

| Q             | A
| ------------- | ---
| Branch?       | 7.1
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| License       | MIT

Before some months we added the `MacAddress` contraint to v7.1, see also #51862

This MR also adds support for validating unicast/multicast, local/universal or any (default) mac address versions. For more informations, see:
https://en.wikipedia.org/wiki/MAC_address#Ranges_of_group_and_locally_administered_addresses

~~PS: May we should rename `PRIVATE` & `PUBLIC` to `LOCAL` & `UNIVERSAL` to be a bit more consistent with naming in mac address standard. Also then maybe `version` attribute to `type`. .. Just let me know (already prepared changes locally) :-)~~

Commits
-------

16b9210 [Validator] Add support for types (`ALL*`, `LOCAL_*`, `UNIVERSAL_*`, `UNICAST_*`, `MULTICAST_*`, `BROADCAST`) in `MacAddress` constraint
  • Loading branch information
fabpot committed Apr 13, 2024
2 parents 50d7ce0 + 16b9210 commit e4c7068
Show file tree
Hide file tree
Showing 4 changed files with 538 additions and 4 deletions.
41 changes: 41 additions & 0 deletions src/Symfony/Component/Validator/Constraints/MacAddress.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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;
}

0 comments on commit e4c7068

Please sign in to comment.