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 IPv4 address.
11 */
12class IPv4 implements AddressInterface
13{
14    /**
15     * The string representation of the address.
16     *
17     * @var string
18     *
19     * @example '127.0.0.1'
20     */
21    protected $address;
22
23    /**
24     * The byte list of the IP address.
25     *
26     * @var int[]|null
27     */
28    protected $bytes;
29
30    /**
31     * The type of the range of this IP address.
32     *
33     * @var int|null
34     */
35    protected $rangeType;
36
37    /**
38     * A string representation of this address than can be used when comparing addresses and ranges.
39     *
40     * @var string
41     */
42    protected $comparableString;
43
44    /**
45     * An array containing RFC designated address ranges.
46     *
47     * @var array|null
48     */
49    private static $reservedRanges = null;
50
51    /**
52     * Initializes the instance.
53     *
54     * @param string $address
55     */
56    protected function __construct($address)
57    {
58        $this->address = $address;
59        $this->bytes = null;
60        $this->rangeType = null;
61        $this->comparableString = null;
62    }
63
64    /**
65     * Parse a string and returns an IPv4 instance if the string is valid, or null otherwise.
66     *
67     * @param string|mixed $address the address to parse
68     * @param bool $mayIncludePort set to false to avoid parsing addresses with ports
69     *
70     * @return static|null
71     */
72    public static function fromString($address, $mayIncludePort = true)
73    {
74        $result = null;
75        if (is_string($address) && strpos($address, '.')) {
76            $rx = '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})';
77            if ($mayIncludePort) {
78                $rx .= '(?::\d+)?';
79            }
80            $matches = null;
81            if (preg_match('/^'.$rx.'$/', $address, $matches)) {
82                $ok = true;
83                $nums = array();
84                for ($i = 1; $ok && $i <= 4; ++$i) {
85                    $ok = false;
86                    $n = (int) $matches[$i];
87                    if ($n >= 0 && $n <= 255) {
88                        $ok = true;
89                        $nums[] = (string) $n;
90                    }
91                }
92                if ($ok) {
93                    $result = new static(implode('.', $nums));
94                }
95            }
96        }
97
98        return $result;
99    }
100
101    /**
102     * Parse an array of bytes and returns an IPv4 instance if the array is valid, or null otherwise.
103     *
104     * @param int[]|array $bytes
105     *
106     * @return static|null
107     */
108    public static function fromBytes(array $bytes)
109    {
110        $result = null;
111        if (count($bytes) === 4) {
112            $chunks = array_map(
113                function ($byte) {
114                    return (is_int($byte) && $byte >= 0 && $byte <= 255) ? (string) $byte : false;
115                },
116                $bytes
117            );
118            if (in_array(false, $chunks, true) === false) {
119                $result = new static(implode('.', $chunks));
120            }
121        }
122
123        return $result;
124    }
125
126    /**
127     * {@inheritdoc}
128     *
129     * @see \IPLib\Address\AddressInterface::toString()
130     */
131    public function toString($long = false)
132    {
133        if ($long) {
134            return $this->getComparableString();
135        }
136
137        return $this->address;
138    }
139
140    /**
141     * {@inheritdoc}
142     *
143     * @see \IPLib\Address\AddressInterface::__toString()
144     */
145    public function __toString()
146    {
147        return $this->address;
148    }
149
150    /**
151     * {@inheritdoc}
152     *
153     * @see \IPLib\Address\AddressInterface::getBytes()
154     */
155    public function getBytes()
156    {
157        if ($this->bytes === null) {
158            $this->bytes = array_map(
159                function ($chunk) {
160                    return (int) $chunk;
161                },
162                explode('.', $this->address)
163            );
164        }
165
166        return $this->bytes;
167    }
168
169    /**
170     * {@inheritdoc}
171     *
172     * @see \IPLib\Address\AddressInterface::getAddressType()
173     */
174    public function getAddressType()
175    {
176        return Type::T_IPv4;
177    }
178
179    /**
180     * {@inheritdoc}
181     *
182     * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
183     */
184    public static function getDefaultReservedRangeType()
185    {
186        return RangeType::T_PUBLIC;
187    }
188
189    /**
190     * {@inheritdoc}
191     *
192     * @see \IPLib\Address\AddressInterface::getReservedRanges()
193     */
194    public static function getReservedRanges()
195    {
196        if (self::$reservedRanges === null) {
197            $reservedRanges = array();
198            foreach (array(
199                // RFC 5735
200                '0.0.0.0/8' => array(RangeType::T_THISNETWORK, array('0.0.0.0/32' => RangeType::T_UNSPECIFIED)),
201                // RFC 5735
202                '10.0.0.0/8' => array(RangeType::T_PRIVATENETWORK),
203                // RFC 5735
204                '127.0.0.0/8' => array(RangeType::T_LOOPBACK),
205                // RFC 5735
206                '169.254.0.0/16' => array(RangeType::T_LINKLOCAL),
207                // RFC 5735
208                '172.16.0.0/12' => array(RangeType::T_PRIVATENETWORK),
209                // RFC 5735
210                '192.0.0.0/24' => array(RangeType::T_RESERVED),
211                // RFC 5735
212                '192.0.2.0/24' => array(RangeType::T_RESERVED),
213                // RFC 5735
214                '192.88.99.0/24' => array(RangeType::T_ANYCASTRELAY),
215                // RFC 5735
216                '192.168.0.0/16' => array(RangeType::T_PRIVATENETWORK),
217                // RFC 5735
218                '198.18.0.0/15' => array(RangeType::T_RESERVED),
219                // RFC 5735
220                '198.51.100.0/24' => array(RangeType::T_RESERVED),
221                // RFC 5735
222                '203.0.113.0/24' => array(RangeType::T_RESERVED),
223                // RFC 5735
224                '224.0.0.0/4' => array(RangeType::T_MULTICAST),
225                // RFC 5735
226                '240.0.0.0/4' => array(RangeType::T_RESERVED, array('255.255.255.255/32' => RangeType::T_LIMITEDBROADCAST)),
227            ) as $range => $data) {
228                $exceptions = array();
229                if (isset($data[1])) {
230                    foreach ($data[1] as $exceptionRange => $exceptionType) {
231                        $exceptions[] = new AssignedRange(Subnet::fromString($exceptionRange), $exceptionType);
232                    }
233                }
234                $reservedRanges[] = new AssignedRange(Subnet::fromString($range), $data[0], $exceptions);
235            }
236            self::$reservedRanges = $reservedRanges;
237        }
238
239        return self::$reservedRanges;
240    }
241
242    /**
243     * {@inheritdoc}
244     *
245     * @see \IPLib\Address\AddressInterface::getRangeType()
246     */
247    public function getRangeType()
248    {
249        if ($this->rangeType === null) {
250            $rangeType = null;
251            foreach (static::getReservedRanges() as $reservedRange) {
252                $rangeType = $reservedRange->getAddressType($this);
253                if ($rangeType !== null) {
254                    break;
255                }
256            }
257            $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
258        }
259
260        return $this->rangeType;
261    }
262
263    /**
264     * Create an IPv6 representation of this address.
265     *
266     * @return \IPLib\Address\IPv6
267     */
268    public function toIPv6()
269    {
270        $myBytes = $this->getBytes();
271
272        return IPv6::fromString('2002:'.sprintf('%02x', $myBytes[0]).sprintf('%02x', $myBytes[1]).':'.sprintf('%02x', $myBytes[2]).sprintf('%02x', $myBytes[3]).'::');
273    }
274
275    /**
276     * {@inheritdoc}
277     *
278     * @see \IPLib\Address\AddressInterface::getComparableString()
279     */
280    public function getComparableString()
281    {
282        if ($this->comparableString === null) {
283            $chunks = array();
284            foreach ($this->getBytes() as $byte) {
285                $chunks[] = sprintf('%03d', $byte);
286            }
287            $this->comparableString = implode('.', $chunks);
288        }
289
290        return $this->comparableString;
291    }
292
293    /**
294     * {@inheritdoc}
295     *
296     * @see \IPLib\Address\AddressInterface::matches()
297     */
298    public function matches(RangeInterface $range)
299    {
300        return $range->contains($this);
301    }
302
303    /**
304     * {@inheritdoc}
305     *
306     * @see \IPLib\Address\AddressInterface::getNextAddress()
307     */
308    public function getNextAddress()
309    {
310        $overflow = false;
311        $bytes = $this->getBytes();
312        for ($i = count($bytes) - 1; $i >= 0; --$i) {
313            if ($bytes[$i] === 255) {
314                if ($i === 0) {
315                    $overflow = true;
316                    break;
317                }
318                $bytes[$i] = 0;
319            } else {
320                ++$bytes[$i];
321                break;
322            }
323        }
324
325        return $overflow ? null : static::fromBytes($bytes);
326    }
327
328    /**
329     * {@inheritdoc}
330     *
331     * @see \IPLib\Address\AddressInterface::getPreviousAddress()
332     */
333    public function getPreviousAddress()
334    {
335        $overflow = false;
336        $bytes = $this->getBytes();
337        for ($i = count($bytes) - 1; $i >= 0; --$i) {
338            if ($bytes[$i] === 0) {
339                if ($i === 0) {
340                    $overflow = true;
341                    break;
342                }
343                $bytes[$i] = 255;
344            } else {
345                --$bytes[$i];
346                break;
347            }
348        }
349
350        return $overflow ? null : static::fromBytes($bytes);
351    }
352}
353