Converter.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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 BackedEnum;
  13. use Stringable;
  14. use ValueError;
  15. use function filter_var;
  16. use function implode;
  17. use function inet_pton;
  18. use function str_split;
  19. use function strtolower;
  20. use function unpack;
  21. use const FILTER_FLAG_IPV6;
  22. use const FILTER_VALIDATE_IP;
  23. final class Converter
  24. {
  25. /**
  26. * Significant 10 bits of IP to detect Zone ID regular expression pattern.
  27. *
  28. * @var string
  29. */
  30. private const HOST_ADDRESS_BLOCK = "\xfe\x80";
  31. public static function compressIp(BackedEnum|string $ipAddress): string
  32. {
  33. if ($ipAddress instanceof BackedEnum) {
  34. $ipAddress = (string) $ipAddress->value;
  35. }
  36. return match (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  37. false => throw new ValueError('The submitted IP is not a valid IPv6 address.'),
  38. default => strtolower((string) inet_ntop((string) inet_pton($ipAddress))),
  39. };
  40. }
  41. public static function expandIp(BackedEnum|string $ipAddress): string
  42. {
  43. if ($ipAddress instanceof BackedEnum) {
  44. $ipAddress = (string) $ipAddress->value;
  45. }
  46. if (false === filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  47. throw new ValueError('The submitted IP is not a valid IPv6 address.');
  48. }
  49. $hex = (array) unpack('H*hex', (string) inet_pton($ipAddress));
  50. return implode(':', str_split(strtolower($hex['hex'] ?? ''), 4));
  51. }
  52. public static function compress(BackedEnum|Stringable|string|null $host): ?string
  53. {
  54. $components = self::parse($host);
  55. if (null === $components['ipAddress']) {
  56. return match (true) {
  57. null === $host => $host,
  58. $host instanceof BackedEnum => (string) $host->value,
  59. default => (string) $host,
  60. };
  61. }
  62. $components['ipAddress'] = self::compressIp($components['ipAddress']);
  63. return self::build($components);
  64. }
  65. public static function expand(Stringable|string|null $host): ?string
  66. {
  67. $components = self::parse($host);
  68. if (null === $components['ipAddress']) {
  69. return match ($host) {
  70. null => $host,
  71. default => (string) $host,
  72. };
  73. }
  74. $components['ipAddress'] = self::expandIp($components['ipAddress']);
  75. return self::build($components);
  76. }
  77. public static function build(array $components): string
  78. {
  79. $components['ipAddress'] ??= null;
  80. $components['zoneIdentifier'] ??= null;
  81. if (null === $components['ipAddress']) {
  82. return '';
  83. }
  84. return '['.$components['ipAddress'].match ($components['zoneIdentifier']) {
  85. null => '',
  86. default => '%'.$components['zoneIdentifier'],
  87. }.']';
  88. }
  89. /**
  90. * @return array{ipAddress:string|null, zoneIdentifier:string|null}
  91. */
  92. private static function parse(BackedEnum|Stringable|string|null $host): array
  93. {
  94. if (null === $host) {
  95. return ['ipAddress' => null, 'zoneIdentifier' => null];
  96. }
  97. if ($host instanceof BackedEnum) {
  98. $host = $host->value;
  99. }
  100. $host = (string) $host;
  101. if ('' === $host) {
  102. return ['ipAddress' => null, 'zoneIdentifier' => null];
  103. }
  104. if (!str_starts_with($host, '[')) {
  105. return ['ipAddress' => null, 'zoneIdentifier' => null];
  106. }
  107. if (!str_ends_with($host, ']')) {
  108. return ['ipAddress' => null, 'zoneIdentifier' => null];
  109. }
  110. [$ipv6, $zoneIdentifier] = explode('%', substr($host, 1, -1), 2) + [1 => null];
  111. if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  112. return ['ipAddress' => null, 'zoneIdentifier' => null];
  113. }
  114. return match (true) {
  115. null === $zoneIdentifier,
  116. is_string($ipv6) && str_starts_with((string)inet_pton($ipv6), self::HOST_ADDRESS_BLOCK) => ['ipAddress' => $ipv6, 'zoneIdentifier' => $zoneIdentifier],
  117. default => ['ipAddress' => null, 'zoneIdentifier' => null],
  118. };
  119. }
  120. /**
  121. * Tells whether the host is an IPv6.
  122. */
  123. public static function isIpv6(BackedEnum|Stringable|string|null $host): bool
  124. {
  125. return null !== self::parse($host)['ipAddress'];
  126. }
  127. public static function normalize(BackedEnum|Stringable|string|null $host): ?string
  128. {
  129. if ($host instanceof BackedEnum) {
  130. $host = $host->value;
  131. }
  132. if (null === $host || '' === $host) {
  133. return $host;
  134. }
  135. $host = (string) $host;
  136. $components = self::parse($host);
  137. if (null === $components['ipAddress']) {
  138. return strtolower($host);
  139. }
  140. $components['ipAddress'] = strtolower($components['ipAddress']);
  141. return self::build($components);
  142. }
  143. }