Page MenuHomePhabricator

D16973.id40861.diff
No OneTemporary

D16973.id40861.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -249,6 +249,8 @@
'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php',
'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php',
+ 'PhutilIPv4Address' => 'ip/PhutilIPv4Address.php',
+ 'PhutilIPv6Address' => 'ip/PhutilIPv6Address.php',
'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php',
'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php',
@@ -853,6 +855,8 @@
'PhutilINIParserException' => 'Exception',
'PhutilIPAddress' => 'Phobject',
'PhutilIPAddressTestCase' => 'PhutilTestCase',
+ 'PhutilIPv4Address' => 'PhutilIPAddress',
+ 'PhutilIPv6Address' => 'PhutilIPAddress',
'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache',
'PhutilInteractiveEditor' => 'Phobject',
'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException',
diff --git a/src/ip/PhutilIPAddress.php b/src/ip/PhutilIPAddress.php
--- a/src/ip/PhutilIPAddress.php
+++ b/src/ip/PhutilIPAddress.php
@@ -1,88 +1,42 @@
<?php
/**
- * Represent and manipulate IP addresses.
- *
- * NOTE: This class only supports IPv4 for now.
+ * Represent and manipulate IPv4 and IPv6 addresses.
*/
-final class PhutilIPAddress extends Phobject {
-
- private $ip;
- private $bits;
+abstract class PhutilIPAddress
+ extends Phobject {
private function __construct() {
// <private>
}
+ abstract public function toBits();
+ abstract public function getAddress();
+
public static function newAddress($in) {
if ($in instanceof PhutilIPAddress) {
- return $in;
+ return clone $in;
}
- return self::newFromString($in);
- }
-
- private static function newFromString($str) {
- $matches = null;
- $ok = preg_match('(^(\d+)\.(\d+)\.(\d+).(\d+)\z)', $str, $matches);
- if (!$ok) {
- throw new Exception(
- pht(
- 'IP address "%s" is not properly formatted. Expected an IP '.
- 'address like "%s".',
- $str,
- '23.45.67.89'));
+ try {
+ return PhutilIPv4Address::newFromString($in);
+ } catch (Exception $ex) {
+ // Continue, trying the address as IPv6 instead.
}
- $parts = array_slice($matches, 1);
- foreach ($parts as $part) {
- if (preg_match('/^0\d/', $part)) {
- throw new Exception(
- pht(
- 'IP address "%s" is not properly formatted. Address segments '.
- 'should have no leading zeroes, but segment "%s" has a leading '.
- 'zero.',
- $str,
- $part));
- }
-
- $value = (int)$part;
- if ($value < 0 || $value > 255) {
- throw new Exception(
- pht(
- 'IP address "%s" is not properly formatted. Address segments '.
- 'should be between 0 and 255, inclusive, but segment "%s" has '.
- 'a value outside of this range.',
- $str,
- $part));
- }
- }
-
- $obj = new PhutilIPAddress();
- $obj->ip = $str;
-
- return $obj;
- }
-
- public function toBits() {
- if ($this->bits === null) {
- $bits = '';
- foreach (explode('.', $this->ip) as $part) {
- $value = (int)$part;
- for ($ii = 7; $ii >= 0; $ii--) {
- $mask = (1 << $ii);
- if (($value & $mask) === $mask) {
- $bits .= '1';
- } else {
- $bits .= '0';
- }
- }
- }
-
- $this->bits = $bits;
+ try {
+ return PhutilIPv6Address::newFromString($in);
+ } catch (Exception $ex) {
+ // Continue, throwing a more tailored exception below.
}
- return $this->bits;
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted. Expected an IPv4 address '.
+ 'like "%s", or an IPv6 address like "%s".',
+ $in,
+ '23.45.67.89',
+ '2345:6789:0123:abcd::'));
}
}
diff --git a/src/ip/PhutilIPAddress.php b/src/ip/PhutilIPv4Address.php
copy from src/ip/PhutilIPAddress.php
copy to src/ip/PhutilIPv4Address.php
--- a/src/ip/PhutilIPAddress.php
+++ b/src/ip/PhutilIPv4Address.php
@@ -1,11 +1,9 @@
<?php
/**
- * Represent and manipulate IP addresses.
- *
- * NOTE: This class only supports IPv4 for now.
+ * Represent and manipulate IPv4 addresses.
*/
-final class PhutilIPAddress extends Phobject {
+final class PhutilIPv4Address extends PhutilIPAddress {
private $ip;
private $bits;
@@ -14,21 +12,17 @@
// <private>
}
- public static function newAddress($in) {
- if ($in instanceof PhutilIPAddress) {
- return $in;
- }
-
- return self::newFromString($in);
+ public function getAddress() {
+ return $this->ip;
}
- private static function newFromString($str) {
+ protected static function newFromString($str) {
$matches = null;
$ok = preg_match('(^(\d+)\.(\d+)\.(\d+).(\d+)\z)', $str, $matches);
if (!$ok) {
throw new Exception(
pht(
- 'IP address "%s" is not properly formatted. Expected an IP '.
+ 'IP address "%s" is not properly formatted. Expected an IPv4 '.
'address like "%s".',
$str,
'23.45.67.89'));
@@ -58,7 +52,7 @@
}
}
- $obj = new PhutilIPAddress();
+ $obj = new self();
$obj->ip = $str;
return $obj;
diff --git a/src/ip/PhutilIPv6Address.php b/src/ip/PhutilIPv6Address.php
new file mode 100644
--- /dev/null
+++ b/src/ip/PhutilIPv6Address.php
@@ -0,0 +1,208 @@
+<?php
+
+/**
+ * Represent and manipulate IPv6 addresses.
+ */
+final class PhutilIPv6Address extends PhutilIPAddress {
+
+ private $values;
+ private $displayAddress;
+ private $bits;
+
+ private function __construct() {
+ // <private>
+ }
+
+ protected static function newFromString($str) {
+ $parts = explode(':', $str);
+ if (count($parts) > 8) {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted: is has too many '.
+ 'parts. Expected a maximum of 7 colons, like "%s".',
+ $str,
+ '1:2:3:4:a:b:c:d'));
+ }
+
+ if (count($parts) < 3) {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formated: it has too few '.
+ 'parts. Expected a minimum of 2 colons, like "%s".',
+ $str,
+ '::1'));
+ }
+
+ // Look for leading or trailing empty parts. These are valid if the string
+ // begins or ends like "::", "::1", or "1::", but not valid otherwise.
+ $has_omission = false;
+ if ($str === '::') {
+ $parts = array(null);
+ $has_omission = true;
+ } else if ($parts[0] === '') {
+ if ($parts[1] === '') {
+ unset($parts[1]);
+ $parts[0] = null;
+ $parts = array_values($parts);
+ $has_omission = true;
+ } else {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted: an address with '.
+ 'omitted leading sements must begin with "::".',
+ $str));
+ }
+ } else if (last($parts) === '') {
+ if ($parts[count($parts) - 2] === '') {
+ array_pop($parts);
+ $parts[count($parts) - 1] = null;
+ $parts = array_values($parts);
+ $has_omission = true;
+ } else {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted: an address with '.
+ 'omitted trailing segments must end with "::".',
+ $str));
+ }
+ }
+
+ foreach ($parts as $idx => $part) {
+ if ($part !== '') {
+ continue;
+ }
+
+ if ($has_omission) {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted: an address may '.
+ 'only contain a maximum of one subsequence omitted with "::".',
+ $str));
+ }
+
+ $has_omission = true;
+ $parts[$idx] = null;
+ }
+
+ if (!$has_omission) {
+ if (count($parts) !== 8) {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted: an address must '.
+ 'contain exactly 8 segments, or omit a subsequence of segments '.
+ 'with "::".',
+ $str));
+ }
+ }
+
+ $values = array();
+ foreach ($parts as $idx => $part) {
+ // This is a "::" segment, so fill in any missing values with 0.
+ if ($part === null) {
+ for ($ii = count($parts); $ii <= 8; $ii++) {
+ $values[] = 0;
+ }
+ continue;
+ }
+
+ if (!preg_match('/^[0-9a-fA-F]{1,4}\z/', $part)) {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted: the segments of '.
+ 'an address must be hexadecimal values between "0000" and "ffff", '.
+ 'inclusive. Segment "%s" is not.',
+ $str,
+ $part));
+ }
+
+ $values[] = (int)hexdec($part);
+ }
+
+ $obj = new self();
+ $obj->values = $values;
+
+ return $obj;
+ }
+
+ public function getAddress() {
+ if ($this->displayAddress === null) {
+ // Find the longest consecutive sequence of "0" values. We want to
+ // collapse this into "::".
+ $longest_run = 0;
+ $longest_index = 0;
+ $current_run = null;
+ $current_index = null;
+ foreach ($this->values as $idx => $value) {
+ if ($value !== 0) {
+ $current_run = null;
+ continue;
+ }
+
+ if ($current_run === null) {
+ $current_run = 1;
+ $current_index = $idx;
+ } else {
+ $current_run++;
+ }
+
+ if ($current_run > $longest_run) {
+ $longest_run = $current_run;
+ $longest_index = $current_index;
+ }
+ }
+
+ // Render the segments of the IPv6 address, omitting the longest run
+ // of consecutive "0" segments.
+ $pieces = array();
+ for ($idx = 0; $idx < count($this->values); $idx++) {
+ $value = $this->values[$idx];
+
+ if ($idx === $longest_index) {
+ if ($longest_run > 1) {
+ $pieces[] = null;
+ $idx += ($longest_run - 1);
+ continue;
+ }
+ }
+
+ $pieces[] = dechex($value);
+ }
+
+ // If the omitted segment is at the beginning or end of the address, add
+ // an extra piece so we get the leading or trailing "::" when we implode
+ // the pieces.
+ if (head($pieces) === null) {
+ array_unshift($pieces, null);
+ }
+
+ if (last($pieces) === null) {
+ $pieces[] = null;
+ }
+
+ $this->displayAddress = implode(':', $pieces);
+ }
+
+ return $this->displayAddress;
+ }
+
+ public function toBits() {
+ if ($this->bits === null) {
+ $bits = '';
+ foreach ($this->values as $value) {
+ for ($ii = 15; $ii >= 0; $ii--) {
+ $mask = (1 << $ii);
+ if (($value & $mask) === $mask) {
+ $bits .= '1';
+ } else {
+ $bits .= '0';
+ }
+ }
+ }
+
+ $this->bits = $bits;
+ }
+
+ return $this->bits;
+ }
+
+}
diff --git a/src/ip/__tests__/PhutilIPAddressTestCase.php b/src/ip/__tests__/PhutilIPAddressTestCase.php
--- a/src/ip/__tests__/PhutilIPAddressTestCase.php
+++ b/src/ip/__tests__/PhutilIPAddressTestCase.php
@@ -2,7 +2,7 @@
final class PhutilIPAddressTestCase extends PhutilTestCase {
- public function testValidIPAddresses() {
+ public function testValidIPv4Addresses() {
$cases = array(
// Valid IP.
'1.2.3.4' => true,
@@ -36,11 +36,52 @@
$this->assertEqual(
$expect,
!($caught instanceof Exception),
- 'PhutilIPAddress['.$input.']');
+ 'PhutilIPv4Address['.$input.']');
}
}
- public function testIPAddressToBits() {
+ public function testValidIPv6Addresses() {
+ $cases = array(
+ '::' => true,
+ '::1' => true,
+ '1::' => true,
+ '1::1' => true,
+ '1:2:3:4:5:6:7:8' => true,
+ '1:2:3::5:6:7:8' => true,
+ '1:2:3::6:7:8' => true,
+
+ // No nonsense.
+ 'quack:duck' => false,
+ '11111:22222::' => false,
+
+
+ // Too long.
+ '1:2:3:4:5:6:7:8:9' => false,
+
+ // Too short.
+ '1:2:3' => false,
+
+ // Too many omitted segments.
+ '1:2:3:::7:8:9' => false,
+ '1::3::7:8:9' => false,
+ );
+
+ foreach ($cases as $input => $expect) {
+ $caught = null;
+ try {
+ PhutilIPAddress::newAddress($input);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertEqual(
+ $expect,
+ !($caught instanceof Exception),
+ 'PhutilIPv6Address['.$input.']');
+ }
+ }
+
+ public function testIPv4AddressToBits() {
$cases = array(
'0.0.0.0' => '00000000000000000000000000000000',
'255.255.255.255' => '11111111111111111111111111111111',
@@ -55,7 +96,89 @@
$this->assertEqual(
$expect,
$actual,
- 'PhutilIPAddress['.$input.']->toBits()');
+ 'PhutilIPv4Address['.$input.']->toBits()');
+ }
+ }
+
+ public function testIPv6AddressToBits() {
+ $cases = array(
+ '::' =>
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000',
+ '::1' =>
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000001',
+ '1::' =>
+ '0000000000000001 0000000000000000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000',
+ '::ffff:c000:0280' =>
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 1111111111111111'.
+ PhutilIPAddress::newAddress('192.0.2.128')->toBits(),
+ '21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A' =>
+ '0010000111011010 0000000011010011'.
+ '0000000000000000 0010111100111011'.
+ '0000001010101010 0000000011111111'.
+ '1111111000101000 1001110001011010',
+ '2001:db8::1' =>
+ '0010000000000001 0000110110111000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000000'.
+ '0000000000000000 0000000000000001',
+
+ );
+
+ foreach ($cases as $input => $expect) {
+ // Remove any spaces, these are just to make the tests above easier to
+ // read.
+ $expect = str_replace(' ', '', $expect);
+
+ $actual = PhutilIPAddress::newAddress($input)->toBits();
+ $this->assertEqual(
+ $expect,
+ $actual,
+ 'PhutilIPv6Address['.$input.']->toBits()');
+ }
+ }
+
+ public function testIPv6AddressToAddress() {
+ $cases = array(
+ '::' => '::',
+ '::1' => '::1',
+ '::01' => '::1',
+ '0::0001' => '::1',
+ '0000::0001' => '::1',
+ '0000:0000::001' => '::1',
+
+ '1::' => '1::',
+ '01::' => '1::',
+ '01::0' => '1::',
+ '0001::0000' => '1::',
+
+ '1:0::0:2' => '1::2',
+ '1::0:2' => '1::2',
+ '1:0::2' => '1::2',
+
+ 'CAFE::' => 'cafe::',
+ '0000:aBe:0:0:1::' => '0:abe:0:0:1::',
+
+ '1:0:0:0:2:0:0:0' => '1::2:0:0:0',
+ '1:0:0:2:0:0:0:0' => '1:0:0:2::',
+ );
+
+ foreach ($cases as $input => $expect) {
+ $actual = PhutilIPAddress::newAddress($input)->getAddress();
+ $this->assertEqual(
+ $expect,
+ $actual,
+ 'PhutilIPv6Address['.$input.']->getAddress()');
}
}

File Metadata

Mime Type
text/plain
Expires
Tue, Mar 11, 10:10 AM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7518574
Default Alt Text
D16973.id40861.diff (15 KB)

Event Timeline