Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15345407
D16973.id40861.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
15 KB
Referenced Files
None
Subscribers
None
D16973.id40861.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D16973: Implement basic IPv6 address parsing support
Attached
Detach File
Event Timeline
Log In to Comment