Converter.php 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. <?php
  2. /**
  3. * League.Uri (https://uri.thephpleague.com)
  4. *
  5. * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. declare(strict_types=1);
  11. namespace League\Uri\IPv6;
  12. use Stringable;
  13. use ValueError;
  14. use function filter_var;
  15. use function implode;
  16. use function inet_pton;
  17. use function str_split;
  18. use function strtolower;
  19. use function unpack;
  20. use const FILTER_FLAG_IPV6;
  21. use const FILTER_VALIDATE_IP;
  22. final class Converter
  23. {
  24. /**
  25. * Significant 10 bits of IP to detect Zone ID regular expression pattern.
  26. *
  27. * @var string
  28. */
  29. private const HOST_ADDRESS_BLOCK = "\xfe\x80";
  30. public static function compressIp(string $ipAddress): string
  31. {
  32. return match (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  33. false => throw new ValueError('The submitted IP is not a valid IPv6 address.'),
  34. default => strtolower((string) inet_ntop((string) inet_pton($ipAddress))),
  35. };
  36. }
  37. public static function expandIp(string $ipAddress): string
  38. {
  39. if (false === filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  40. throw new ValueError('The submitted IP is not a valid IPv6 address.');
  41. }
  42. $hex = (array) unpack('H*hex', (string) inet_pton($ipAddress));
  43. return implode(':', str_split(strtolower($hex['hex'] ?? ''), 4));
  44. }
  45. public static function compress(Stringable|string|null $host): ?string
  46. {
  47. $components = self::parse($host);
  48. if (null === $components['ipAddress']) {
  49. return match ($host) {
  50. null => $host,
  51. default => (string) $host,
  52. };
  53. }
  54. $components['ipAddress'] = self::compressIp($components['ipAddress']);
  55. return self::build($components);
  56. }
  57. public static function expand(Stringable|string|null $host): ?string
  58. {
  59. $components = self::parse($host);
  60. if (null === $components['ipAddress']) {
  61. return match ($host) {
  62. null => $host,
  63. default => (string) $host,
  64. };
  65. }
  66. $components['ipAddress'] = self::expandIp($components['ipAddress']);
  67. return self::build($components);
  68. }
  69. private static function build(array $components): string
  70. {
  71. $components['ipAddress'] ??= null;
  72. $components['zoneIdentifier'] ??= null;
  73. if (null === $components['ipAddress']) {
  74. return '';
  75. }
  76. return '['.$components['ipAddress'].match ($components['zoneIdentifier']) {
  77. null => '',
  78. default => '%'.$components['zoneIdentifier'],
  79. }.']';
  80. }
  81. /**]
  82. * @param Stringable|string|null $host
  83. *
  84. * @return array{ipAddress:string|null, zoneIdentifier:string|null}
  85. */
  86. private static function parse(Stringable|string|null $host): array
  87. {
  88. if (null === $host) {
  89. return ['ipAddress' => null, 'zoneIdentifier' => null];
  90. }
  91. $host = (string) $host;
  92. if ('' === $host) {
  93. return ['ipAddress' => null, 'zoneIdentifier' => null];
  94. }
  95. if (!str_starts_with($host, '[')) {
  96. return ['ipAddress' => null, 'zoneIdentifier' => null];
  97. }
  98. if (!str_ends_with($host, ']')) {
  99. return ['ipAddress' => null, 'zoneIdentifier' => null];
  100. }
  101. [$ipv6, $zoneIdentifier] = explode('%', substr($host, 1, -1), 2) + [1 => null];
  102. if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  103. return ['ipAddress' => null, 'zoneIdentifier' => null];
  104. }
  105. return match (true) {
  106. null === $zoneIdentifier,
  107. is_string($ipv6) && str_starts_with((string)inet_pton($ipv6), self::HOST_ADDRESS_BLOCK) => ['ipAddress' => $ipv6, 'zoneIdentifier' => $zoneIdentifier],
  108. default => ['ipAddress' => null, 'zoneIdentifier' => null],
  109. };
  110. }
  111. }