1<?php
2
3namespace IPLib\Range;
4
5use IPLib\Address\AddressInterface;
6use IPLib\Address\IPv4;
7use IPLib\Address\IPv6;
8use IPLib\Address\Type as AddressType;
9use IPLib\Factory;
10
11/**
12 * Represents an address range in subnet format (eg CIDR).
13 *
14 * @example 127.0.0.1/32
15 * @example ::/8
16 */
17class Subnet implements RangeInterface
18{
19    /**
20     * Starting address of the range.
21     *
22     * @var \IPLib\Address\AddressInterface
23     */
24    protected $fromAddress;
25
26    /**
27     * Final address of the range.
28     *
29     * @var \IPLib\Address\AddressInterface
30     */
31    protected $toAddress;
32
33    /**
34     * Number of the same bits of the range.
35     *
36     * @var int
37     */
38    protected $networkPrefix;
39
40    /**
41     * The type of the range of this IP range.
42     *
43     * @var int|null
44     */
45    protected $rangeType;
46
47    /**
48     * The 6to4 address IPv6 address range.
49     *
50     * @var self|null
51     */
52    private static $sixToFour;
53
54    /**
55     * Initializes the instance.
56     *
57     * @param \IPLib\Address\AddressInterface $fromAddress
58     * @param \IPLib\Address\AddressInterface $toAddress
59     * @param int $networkPrefix
60     *
61     * @internal
62     */
63    public function __construct(AddressInterface $fromAddress, AddressInterface $toAddress, $networkPrefix)
64    {
65        $this->fromAddress = $fromAddress;
66        $this->toAddress = $toAddress;
67        $this->networkPrefix = $networkPrefix;
68    }
69
70    /**
71     * Try get the range instance starting from its string representation.
72     *
73     * @param string|mixed $range
74     *
75     * @return static|null
76     */
77    public static function fromString($range)
78    {
79        $result = null;
80        if (is_string($range)) {
81            $parts = explode('/', $range);
82            if (count($parts) === 2) {
83                $address = Factory::addressFromString($parts[0]);
84                if ($address !== null) {
85                    if (preg_match('/^[0-9]{1,9}$/', $parts[1])) {
86                        $networkPrefix = (int) $parts[1];
87                        if ($networkPrefix >= 0) {
88                            $addressBytes = $address->getBytes();
89                            $totalBytes = count($addressBytes);
90                            $numDifferentBits = $totalBytes * 8 - $networkPrefix;
91                            if ($numDifferentBits >= 0) {
92                                $numSameBytes = $networkPrefix >> 3;
93                                $sameBytes = array_slice($addressBytes, 0, $numSameBytes);
94                                $differentBytesStart = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 0);
95                                $differentBytesEnd = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 255);
96                                $startSameBits = $networkPrefix % 8;
97                                if ($startSameBits !== 0) {
98                                    $varyingByte = $addressBytes[$numSameBytes];
99                                    $differentBytesStart[0] = $varyingByte & bindec(str_pad(str_repeat('1', $startSameBits), 8, '0', STR_PAD_RIGHT));
100                                    $differentBytesEnd[0] = $differentBytesStart[0] + bindec(str_repeat('1', 8 - $startSameBits));
101                                }
102                                $result = new static(
103                                    Factory::addressFromBytes(array_merge($sameBytes, $differentBytesStart)),
104                                    Factory::addressFromBytes(array_merge($sameBytes, $differentBytesEnd)),
105                                    $networkPrefix
106                                );
107                            }
108                        }
109                    }
110                }
111            }
112        }
113
114        return $result;
115    }
116
117    /**
118     * {@inheritdoc}
119     *
120     * @see \IPLib\Range\RangeInterface::toString()
121     */
122    public function toString($long = false)
123    {
124        return $this->fromAddress->toString($long).'/'.$this->networkPrefix;
125    }
126
127    /**
128     * {@inheritdoc}
129     *
130     * @see \IPLib\Range\RangeInterface::__toString()
131     */
132    public function __toString()
133    {
134        return $this->toString();
135    }
136
137    /**
138     * {@inheritdoc}
139     *
140     * @see \IPLib\Range\RangeInterface::getAddressType()
141     */
142    public function getAddressType()
143    {
144        return $this->fromAddress->getAddressType();
145    }
146
147    /**
148     * {@inheritdoc}
149     *
150     * @see \IPLib\Range\RangeInterface::getRangeType()
151     */
152    public function getRangeType()
153    {
154        if ($this->rangeType === null) {
155            $addressType = $this->getAddressType();
156            if ($addressType === AddressType::T_IPv6 && static::get6to4()->containsRange($this)) {
157                $this->rangeType = Factory::rangeFromBoundaries($this->fromAddress->toIPv4(), $this->toAddress->toIPv4())->getRangeType();
158            } else {
159                switch ($addressType) {
160                    case AddressType::T_IPv4:
161                        $defaultType = IPv4::getDefaultReservedRangeType();
162                        $reservedRanges = IPv4::getReservedRanges();
163                        break;
164                    case AddressType::T_IPv6:
165                        $defaultType = IPv6::getDefaultReservedRangeType();
166                        $reservedRanges = IPv6::getReservedRanges();
167                        break;
168                    default:
169                        throw new \Exception('@todo'); // @codeCoverageIgnore
170                }
171                $rangeType = null;
172                foreach ($reservedRanges as $reservedRange) {
173                    $rangeType = $reservedRange->getRangeType($this);
174                    if ($rangeType !== null) {
175                        break;
176                    }
177                }
178                $this->rangeType = $rangeType === null ? $defaultType : $rangeType;
179            }
180        }
181
182        return $this->rangeType === false ? null : $this->rangeType;
183    }
184
185    /**
186     * {@inheritdoc}
187     *
188     * @see \IPLib\Range\RangeInterface::contains()
189     */
190    public function contains(AddressInterface $address)
191    {
192        $result = false;
193        if ($address->getAddressType() === $this->getAddressType()) {
194            $cmp = $address->getComparableString();
195            $from = $this->getComparableStartString();
196            if ($cmp >= $from) {
197                $to = $this->getComparableEndString();
198                if ($cmp <= $to) {
199                    $result = true;
200                }
201            }
202        }
203
204        return $result;
205    }
206
207    /**
208     * {@inheritdoc}
209     *
210     * @see \IPLib\Range\RangeInterface::containsRange()
211     */
212    public function containsRange(RangeInterface $range)
213    {
214        $result = false;
215        if ($range->getAddressType() === $this->getAddressType()) {
216            $myStart = $this->getComparableStartString();
217            $itsStart = $range->getComparableStartString();
218            if ($itsStart >= $myStart) {
219                $myEnd = $this->getComparableEndString();
220                $itsEnd = $range->getComparableEndString();
221                if ($itsEnd <= $myEnd) {
222                    $result = true;
223                }
224            }
225        }
226
227        return $result;
228    }
229
230    /**
231     * {@inheritdoc}
232     *
233     * @see \IPLib\Range\RangeInterface::getStartAddress()
234     */
235    public function getStartAddress()
236    {
237        return $this->fromAddress;
238    }
239
240    /**
241     * {@inheritdoc}
242     *
243     * @see \IPLib\Range\RangeInterface::getEndAddress()
244     */
245    public function getEndAddress()
246    {
247        return $this->toAddress;
248    }
249
250    /**
251     * {@inheritdoc}
252     *
253     * @see \IPLib\Range\RangeInterface::getComparableStartString()
254     */
255    public function getComparableStartString()
256    {
257        return $this->fromAddress->getComparableString();
258    }
259
260    /**
261     * {@inheritdoc}
262     *
263     * @see \IPLib\Range\RangeInterface::getComparableEndString()
264     */
265    public function getComparableEndString()
266    {
267        return $this->toAddress->getComparableString();
268    }
269
270    /**
271     * Get the 6to4 address IPv6 address range.
272     *
273     * @return self
274     */
275    public static function get6to4()
276    {
277        if (self::$sixToFour === null) {
278            self::$sixToFour = self::fromString('2002::/16');
279        }
280
281        return self::$sixToFour;
282    }
283
284    /**
285     * Get subnet prefix.
286     *
287     * @return int
288     */
289    public function getNetworkPrefix()
290    {
291        return $this->networkPrefix;
292    }
293
294    /**
295     * Get the pattern representation (if applicable) of this range.
296     *
297     * @return \IPLib\Range\Pattern|null return NULL if this range can't be represented by a pattern notation
298     */
299    public function asPattern()
300    {
301        $address = $this->getStartAddress();
302        $networkPrefix = $this->getNetworkPrefix();
303        switch ($address->getAddressType()) {
304            case AddressType::T_IPv4:
305                return $networkPrefix % 8 === 0 ? new Pattern($address, $address, 4 - $networkPrefix / 8) : null;
306            case AddressType::T_IPv6:
307                return $networkPrefix % 16 === 0 ? new Pattern($address, $address, 8 - $networkPrefix / 16) : null;
308        }
309    }
310
311    /**
312     * {@inheritdoc}
313     *
314     * @see \IPLib\Range\RangeInterface::getSubnetMask()
315     */
316    public function getSubnetMask()
317    {
318        if ($this->getAddressType() !== AddressType::T_IPv4) {
319            return null;
320        }
321        $bytes = array();
322        $prefix = $this->getNetworkPrefix();
323        while ($prefix >= 8) {
324            $bytes[] = 255;
325            $prefix -= 8;
326        }
327        if ($prefix !== 0) {
328            $bytes[] = bindec(str_pad(str_repeat('1', $prefix), 8, '0'));
329        }
330        $bytes = array_pad($bytes, 4, 0);
331
332        return IPv4::fromBytes($bytes);
333    }
334}
335