1<?php
2
3namespace IPLib\Address;
4
5use IPLib\Range\RangeInterface;
6use IPLib\Range\Subnet;
7use IPLib\Range\Type as RangeType;
8
9/**
10 * An IPv6 address.
11 */
12class IPv6 implements AddressInterface
13{
14    /**
15     * The long string representation of the address.
16     *
17     * @var string
18     *
19     * @example '0000:0000:0000:0000:0000:0000:0000:0001'
20     */
21    protected $longAddress;
22
23    /**
24     * The long string representation of the address.
25     *
26     * @var string|null
27     *
28     * @example '::1'
29     */
30    protected $shortAddress;
31
32    /**
33     * The byte list of the IP address.
34     *
35     * @var int[]|null
36     */
37    protected $bytes;
38
39    /**
40     * The word list of the IP address.
41     *
42     * @var int[]|null
43     */
44    protected $words;
45
46    /**
47     * The type of the range of this IP address.
48     *
49     * @var int|null
50     */
51    protected $rangeType;
52
53    /**
54     * An array containing RFC designated address ranges.
55     *
56     * @var array|null
57     */
58    private static $reservedRanges = null;
59
60    /**
61     * Initializes the instance.
62     *
63     * @param string $longAddress
64     */
65    public function __construct($longAddress)
66    {
67        $this->longAddress = $longAddress;
68        $this->shortAddress = null;
69        $this->bytes = null;
70        $this->words = null;
71        $this->rangeType = null;
72    }
73
74    /**
75     * Parse a string and returns an IPv6 instance if the string is valid, or null otherwise.
76     *
77     * @param string|mixed $address the address to parse
78     * @param bool $mayIncludePort set to false to avoid parsing addresses with ports
79     * @param bool $mayIncludeZoneID set to false to avoid parsing addresses with zone IDs (see RFC 4007)
80     *
81     * @return static|null
82     */
83    public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true)
84    {
85        $result = null;
86        if (is_string($address) && strpos($address, ':') !== false && strpos($address, ':::') === false) {
87            $matches = null;
88            if ($mayIncludePort && $address[0] === '[' && preg_match('/^\[(.+)\]:\d+$/', $address, $matches)) {
89                $address = $matches[1];
90            }
91            if ($mayIncludeZoneID) {
92                $percentagePos = strpos($address, '%');
93                if ($percentagePos > 0) {
94                    $address = substr($address, 0, $percentagePos);
95                }
96            }
97            if (preg_match('/^([0:]+:ffff:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) {
98                // IPv4 embedded in IPv6
99                $address6 = static::fromString($matches[1].'0:0', false);
100                if ($address6 !== null) {
101                    $address4 = IPv4::fromString($matches[2], false);
102                    if ($address4 !== null) {
103                        $bytes4 = $address4->getBytes();
104                        $address6->longAddress = substr($address6->longAddress, 0, -9).sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]);
105                        $result = $address6;
106                    }
107                }
108            } else {
109                if (strpos($address, '::') === false) {
110                    $chunks = explode(':', $address);
111                } else {
112                    $chunks = array();
113                    $parts = explode('::', $address);
114                    if (count($parts) === 2) {
115                        $before = ($parts[0] === '') ? array() : explode(':', $parts[0]);
116                        $after = ($parts[1] === '') ? array() : explode(':', $parts[1]);
117                        $missing = 8 - count($before) - count($after);
118                        if ($missing >= 0) {
119                            $chunks = $before;
120                            if ($missing !== 0) {
121                                $chunks = array_merge($chunks, array_fill(0, $missing, '0'));
122                            }
123                            $chunks = array_merge($chunks, $after);
124                        }
125                    }
126                }
127                if (count($chunks) === 8) {
128                    $nums = array_map(
129                        function ($chunk) {
130                            return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false;
131                        },
132                        $chunks
133                    );
134                    if (!in_array(false, $nums, true)) {
135                        $longAddress = implode(
136                            ':',
137                            array_map(
138                                function ($num) {
139                                    return sprintf('%04x', $num);
140                                },
141                                $nums
142                            )
143                        );
144                        $result = new static($longAddress);
145                    }
146                }
147            }
148        }
149
150        return $result;
151    }
152
153    /**
154     * Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise.
155     *
156     * @param int[]|array $bytes
157     *
158     * @return static|null
159     */
160    public static function fromBytes(array $bytes)
161    {
162        $result = null;
163        if (count($bytes) === 16) {
164            $address = '';
165            for ($i = 0; $i < 16; ++$i) {
166                if ($i !== 0 && $i % 2 === 0) {
167                    $address .= ':';
168                }
169                $byte = $bytes[$i];
170                if (is_int($byte) && $byte >= 0 && $byte <= 255) {
171                    $address .= sprintf('%02x', $byte);
172                } else {
173                    $address = null;
174                    break;
175                }
176            }
177            if ($address !== null) {
178                $result = new static($address);
179            }
180        }
181
182        return $result;
183    }
184
185    /**
186     * Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise.
187     *
188     * @param int[]|array $words
189     *
190     * @return static|null
191     */
192    public static function fromWords(array $words)
193    {
194        $result = null;
195        if (count($words) === 8) {
196            $chunks = array();
197            for ($i = 0; $i < 8; ++$i) {
198                $word = $words[$i];
199                if (is_int($word) && $word >= 0 && $word <= 0xffff) {
200                    $chunks[] = sprintf('%04x', $word);
201                } else {
202                    $chunks = null;
203                    break;
204                }
205            }
206            if ($chunks !== null) {
207                $result = new static(implode(':', $chunks));
208            }
209        }
210
211        return $result;
212    }
213
214    /**
215     * {@inheritdoc}
216     *
217     * @see \IPLib\Address\AddressInterface::toString()
218     */
219    public function toString($long = false)
220    {
221        if ($long) {
222            $result = $this->longAddress;
223        } else {
224            if ($this->shortAddress === null) {
225                if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
226                    $lastBytes = array_slice($this->getBytes(), -4);
227                    $this->shortAddress = '::ffff:'.implode('.', $lastBytes);
228                } else {
229                    $chunks = array_map(
230                        function ($word) {
231                            return dechex($word);
232                        },
233                        $this->getWords()
234                    );
235                    $shortAddress = implode(':', $chunks);
236                    $matches = null;
237                    for ($i = 8; $i > 1; --$i) {
238                        $search = '(?:^|:)'.rtrim(str_repeat('0:', $i), ':').'(?:$|:)';
239                        if (preg_match('/^(.*?)'.$search.'(.*)$/', $shortAddress, $matches)) {
240                            $shortAddress = $matches[1].'::'.$matches[2];
241                            break;
242                        }
243                    }
244                    $this->shortAddress = $shortAddress;
245                }
246            }
247            $result = $this->shortAddress;
248        }
249
250        return $result;
251    }
252
253    /**
254     * {@inheritdoc}
255     *
256     * @see \IPLib\Address\AddressInterface::__toString()
257     */
258    public function __toString()
259    {
260        return $this->toString();
261    }
262
263    /**
264     * {@inheritdoc}
265     *
266     * @see \IPLib\Address\AddressInterface::getBytes()
267     */
268    public function getBytes()
269    {
270        if ($this->bytes === null) {
271            $bytes = array();
272            foreach ($this->getWords() as $word) {
273                $bytes[] = $word >> 8;
274                $bytes[] = $word & 0xff;
275            }
276            $this->bytes = $bytes;
277        }
278
279        return $this->bytes;
280    }
281
282    /**
283     * Get the word list of the IP address.
284     *
285     * @return int[]
286     */
287    public function getWords()
288    {
289        if ($this->words === null) {
290            $this->words = array_map(
291                function ($chunk) {
292                    return hexdec($chunk);
293                },
294                explode(':', $this->longAddress)
295            );
296        }
297
298        return $this->words;
299    }
300
301    /**
302     * {@inheritdoc}
303     *
304     * @see \IPLib\Address\AddressInterface::getAddressType()
305     */
306    public function getAddressType()
307    {
308        return Type::T_IPv6;
309    }
310
311    /**
312     * {@inheritdoc}
313     *
314     * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
315     */
316    public static function getDefaultReservedRangeType()
317    {
318        return RangeType::T_RESERVED;
319    }
320
321    /**
322     * {@inheritdoc}
323     *
324     * @see \IPLib\Address\AddressInterface::getReservedRanges()
325     */
326    public static function getReservedRanges()
327    {
328        if (self::$reservedRanges === null) {
329            $reservedRanges = array();
330            foreach (array(
331                // RFC 4291
332                '::/128' => array(RangeType::T_UNSPECIFIED),
333                // RFC 4291
334                '::1/128' => array(RangeType::T_LOOPBACK),
335                // RFC 4291
336                '100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)),
337                //'2002::/16' => array(RangeType::),
338                // RFC 4291
339                '2000::/3' => array(RangeType::T_PUBLIC),
340                // RFC 4193
341                'fc00::/7' => array(RangeType::T_PRIVATENETWORK),
342                // RFC 4291
343                'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST),
344                // RFC 4291
345                'ff00::/8' => array(RangeType::T_MULTICAST),
346                // RFC 4291
347                //'::/8' => array(RangeType::T_RESERVED),
348                // RFC 4048
349                //'200::/7' => array(RangeType::T_RESERVED),
350                // RFC 4291
351                //'400::/6' => array(RangeType::T_RESERVED),
352                // RFC 4291
353                //'800::/5' => array(RangeType::T_RESERVED),
354                // RFC 4291
355                //'1000::/4' => array(RangeType::T_RESERVED),
356                // RFC 4291
357                //'4000::/3' => array(RangeType::T_RESERVED),
358                // RFC 4291
359                //'6000::/3' => array(RangeType::T_RESERVED),
360                // RFC 4291
361                //'8000::/3' => array(RangeType::T_RESERVED),
362                // RFC 4291
363                //'a000::/3' => array(RangeType::T_RESERVED),
364                // RFC 4291
365                //'c000::/3' => array(RangeType::T_RESERVED),
366                // RFC 4291
367                //'e000::/4' => array(RangeType::T_RESERVED),
368                // RFC 4291
369                //'f000::/5' => array(RangeType::T_RESERVED),
370                // RFC 4291
371                //'f800::/6' => array(RangeType::T_RESERVED),
372                // RFC 4291
373                //'fe00::/9' => array(RangeType::T_RESERVED),
374                // RFC 3879
375                //'fec0::/10' => array(RangeType::T_RESERVED),
376            ) as $range => $data) {
377                $exceptions = array();
378                if (isset($data[1])) {
379                    foreach ($data[1] as $exceptionRange => $exceptionType) {
380                        $exceptions[] = new AssignedRange(Subnet::fromString($exceptionRange), $exceptionType);
381                    }
382                }
383                $reservedRanges[] = new AssignedRange(Subnet::fromString($range), $data[0], $exceptions);
384            }
385            self::$reservedRanges = $reservedRanges;
386        }
387
388        return self::$reservedRanges;
389    }
390
391    /**
392     * {@inheritdoc}
393     *
394     * @see \IPLib\Address\AddressInterface::getRangeType()
395     */
396    public function getRangeType()
397    {
398        if ($this->rangeType === null) {
399            $ipv4 = $this->toIPv4();
400            if ($ipv4 !== null) {
401                $this->rangeType = $ipv4->getRangeType();
402            } else {
403                $rangeType = null;
404                foreach (static::getReservedRanges() as $reservedRange) {
405                    $rangeType = $reservedRange->getAddressType($this);
406                    if ($rangeType !== null) {
407                        break;
408                    }
409                }
410                $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
411            }
412        }
413
414        return $this->rangeType;
415    }
416
417    /**
418     * Create an IPv4 representation of this address (if possible, otherwise returns null).
419     *
420     * @return \IPLib\Address\IPv4|null
421     */
422    public function toIPv4()
423    {
424        $result = null;
425        if (strpos($this->longAddress, '2002:') === 0) {
426            $result = IPv4::fromBytes(array_slice($this->getBytes(), 2, 4));
427        }
428
429        return $result;
430    }
431
432    /**
433     * {@inheritdoc}
434     *
435     * @see \IPLib\Address\AddressInterface::getComparableString()
436     */
437    public function getComparableString()
438    {
439        return $this->longAddress;
440    }
441
442    /**
443     * {@inheritdoc}
444     *
445     * @see \IPLib\Address\AddressInterface::matches()
446     */
447    public function matches(RangeInterface $range)
448    {
449        return $range->contains($this);
450    }
451
452    /**
453     * {@inheritdoc}
454     *
455     * @see \IPLib\Address\AddressInterface::getNextAddress()
456     */
457    public function getNextAddress()
458    {
459        $overflow = false;
460        $words = $this->getWords();
461        for ($i = count($words) - 1; $i >= 0; --$i) {
462            if ($words[$i] === 0xffff) {
463                if ($i === 0) {
464                    $overflow = true;
465                    break;
466                }
467                $words[$i] = 0;
468            } else {
469                ++$words[$i];
470                break;
471            }
472        }
473
474        return $overflow ? null : static::fromWords($words);
475    }
476
477    /**
478     * {@inheritdoc}
479     *
480     * @see \IPLib\Address\AddressInterface::getPreviousAddress()
481     */
482    public function getPreviousAddress()
483    {
484        $overflow = false;
485        $words = $this->getWords();
486        for ($i = count($words) - 1; $i >= 0; --$i) {
487            if ($words[$i] === 0) {
488                if ($i === 0) {
489                    $overflow = true;
490                    break;
491                }
492                $words[$i] = 0xffff;
493            } else {
494                --$words[$i];
495                break;
496            }
497        }
498
499        return $overflow ? null : static::fromWords($words);
500    }
501}
502