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 pattern format (only ending asterisks are supported).
13 *
14 * @example 127.0.*.*
15 * @example ::/8
16 */
17class Pattern 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 ending asterisks.
35     *
36     * @var int
37     */
38    protected $asterisksCount;
39
40    /**
41     * The type of the range of this IP range.
42     *
43     * @var int|null|false false if this range crosses multiple range types, null if yet to be determined
44     */
45    protected $rangeType;
46
47    /**
48     * Initializes the instance.
49     *
50     * @param \IPLib\Address\AddressInterface $fromAddress
51     * @param \IPLib\Address\AddressInterface $toAddress
52     * @param int $asterisksCount
53     */
54    public function __construct(AddressInterface $fromAddress, AddressInterface $toAddress, $asterisksCount)
55    {
56        $this->fromAddress = $fromAddress;
57        $this->toAddress = $toAddress;
58        $this->asterisksCount = $asterisksCount;
59    }
60
61    /**
62     * Try get the range instance starting from its string representation.
63     *
64     * @param string|mixed $range
65     *
66     * @return static|null
67     */
68    public static function fromString($range)
69    {
70        $result = null;
71        if (is_string($range) && strpos($range, '*') !== false) {
72            $matches = null;
73            if ($range === '*.*.*.*') {
74                $result = new static(IPv4::fromString('0.0.0.0'), IPv4::fromString('255.255.255.255'), 4);
75            } elseif (strpos($range, '.') !== false && preg_match('/^[^*]+((?:\.\*)+)$/', $range, $matches)) {
76                $asterisksCount = strlen($matches[1]) >> 1;
77                if ($asterisksCount > 0) {
78                    $missingDots = 3 - substr_count($range, '.');
79                    if ($missingDots > 0) {
80                        $range .= str_repeat('.*', $missingDots);
81                        $asterisksCount += $missingDots;
82                    }
83                }
84                $fromAddress = IPv4::fromString(str_replace('*', '0', $range));
85                if ($fromAddress !== null) {
86                    $fixedBytes = array_slice($fromAddress->getBytes(), 0, -$asterisksCount);
87                    $otherBytes = array_fill(0, $asterisksCount, 255);
88                    $toAddress = IPv4::fromBytes(array_merge($fixedBytes, $otherBytes));
89                    $result = new static($fromAddress, $toAddress, $asterisksCount);
90                }
91            } elseif ($range === '*:*:*:*:*:*:*:*') {
92                $result = new static(IPv6::fromString('::'), IPv6::fromString('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), 8);
93            } elseif (strpos($range, ':') !== false && preg_match('/^[^*]+((?::\*)+)$/', $range, $matches)) {
94                $asterisksCount = strlen($matches[1]) >> 1;
95                $fromAddress = IPv6::fromString(str_replace('*', '0', $range));
96                if ($fromAddress !== null) {
97                    $fixedWords = array_slice($fromAddress->getWords(), 0, -$asterisksCount);
98                    $otherWords = array_fill(0, $asterisksCount, 0xffff);
99                    $toAddress = IPv6::fromWords(array_merge($fixedWords, $otherWords));
100                    $result = new static($fromAddress, $toAddress, $asterisksCount);
101                }
102            }
103        }
104
105        return $result;
106    }
107
108    /**
109     * {@inheritdoc}
110     *
111     * @see \IPLib\Range\RangeInterface::toString()
112     */
113    public function toString($long = false)
114    {
115        if ($this->asterisksCount === 0) {
116            return $this->fromAddress->toString($long);
117        }
118        switch (true) {
119            case $this->fromAddress instanceof \IPLib\Address\IPv4:
120                $chunks = explode('.', $this->fromAddress->toString());
121                $chunks = array_slice($chunks, 0, -$this->asterisksCount);
122                $chunks = array_pad($chunks, 4, '*');
123                $result = implode('.', $chunks);
124                break;
125            case $this->fromAddress instanceof \IPLib\Address\IPv6:
126                if ($long) {
127                    $chunks = explode(':', $this->fromAddress->toString(true));
128                    $chunks = array_slice($chunks, 0, -$this->asterisksCount);
129                    $chunks = array_pad($chunks, 8, '*');
130                    $result = implode(':', $chunks);
131                } elseif ($this->asterisksCount === 8) {
132                    $result = '*:*:*:*:*:*:*:*';
133                } else {
134                    $bytes = $this->toAddress->getBytes();
135                    $bytes = array_slice($bytes, 0, -$this->asterisksCount * 2);
136                    $bytes = array_pad($bytes, 16, 1);
137                    $address = IPv6::fromBytes($bytes);
138                    $before = substr($address->toString(false), 0, -strlen(':101') * $this->asterisksCount);
139                    $result = $before.str_repeat(':*', $this->asterisksCount);
140                }
141                break;
142            default:
143                throw new \Exception('@todo'); // @codeCoverageIgnore
144        }
145
146        return $result;
147    }
148
149    /**
150     * {@inheritdoc}
151     *
152     * @see \IPLib\Range\RangeInterface::__toString()
153     */
154    public function __toString()
155    {
156        return $this->toString();
157    }
158
159    /**
160     * {@inheritdoc}
161     *
162     * @see \IPLib\Range\RangeInterface::getAddressType()
163     */
164    public function getAddressType()
165    {
166        return $this->fromAddress->getAddressType();
167    }
168
169    /**
170     * {@inheritdoc}
171     *
172     * @see \IPLib\Range\RangeInterface::getRangeType()
173     */
174    public function getRangeType()
175    {
176        if ($this->rangeType === null) {
177            $addressType = $this->getAddressType();
178            if ($addressType === AddressType::T_IPv6 && Subnet::get6to4()->containsRange($this)) {
179                $this->rangeType = Factory::rangeFromBoundaries($this->fromAddress->toIPv4(), $this->toAddress->toIPv4())->getRangeType();
180            } else {
181                switch ($addressType) {
182                    case AddressType::T_IPv4:
183                        $defaultType = IPv4::getDefaultReservedRangeType();
184                        $reservedRanges = IPv4::getReservedRanges();
185                        break;
186                    case AddressType::T_IPv6:
187                        $defaultType = IPv6::getDefaultReservedRangeType();
188                        $reservedRanges = IPv6::getReservedRanges();
189                        break;
190                    default:
191                        throw new \Exception('@todo'); // @codeCoverageIgnore
192                }
193                $rangeType = null;
194                foreach ($reservedRanges as $reservedRange) {
195                    $rangeType = $reservedRange->getRangeType($this);
196                    if ($rangeType !== null) {
197                        break;
198                    }
199                }
200                $this->rangeType = $rangeType === null ? $defaultType : $rangeType;
201            }
202        }
203
204        return $this->rangeType === false ? null : $this->rangeType;
205    }
206
207    /**
208     * {@inheritdoc}
209     *
210     * @see \IPLib\Range\RangeInterface::contains()
211     */
212    public function contains(AddressInterface $address)
213    {
214        $result = false;
215        if ($address->getAddressType() === $this->getAddressType()) {
216            $cmp = $address->getComparableString();
217            $from = $this->getComparableStartString();
218            if ($cmp >= $from) {
219                $to = $this->getComparableEndString();
220                if ($cmp <= $to) {
221                    $result = true;
222                }
223            }
224        }
225
226        return $result;
227    }
228
229    /**
230     * {@inheritdoc}
231     *
232     * @see \IPLib\Range\RangeInterface::containsRange()
233     */
234    public function containsRange(RangeInterface $range)
235    {
236        $result = false;
237        if ($range->getAddressType() === $this->getAddressType()) {
238            $myStart = $this->getComparableStartString();
239            $itsStart = $range->getComparableStartString();
240            if ($itsStart >= $myStart) {
241                $myEnd = $this->getComparableEndString();
242                $itsEnd = $range->getComparableEndString();
243                if ($itsEnd <= $myEnd) {
244                    $result = true;
245                }
246            }
247        }
248
249        return $result;
250    }
251
252    /**
253     * {@inheritdoc}
254     *
255     * @see \IPLib\Range\RangeInterface::getStartAddress()
256     */
257    public function getStartAddress()
258    {
259        return $this->fromAddress;
260    }
261
262    /**
263     * {@inheritdoc}
264     *
265     * @see \IPLib\Range\RangeInterface::getEndAddress()
266     */
267    public function getEndAddress()
268    {
269        return $this->toAddress;
270    }
271
272    /**
273     * {@inheritdoc}
274     *
275     * @see \IPLib\Range\RangeInterface::getComparableStartString()
276     */
277    public function getComparableStartString()
278    {
279        return $this->fromAddress->getComparableString();
280    }
281
282    /**
283     * {@inheritdoc}
284     *
285     * @see \IPLib\Range\RangeInterface::getComparableEndString()
286     */
287    public function getComparableEndString()
288    {
289        return $this->toAddress->getComparableString();
290    }
291
292    /**
293     * Get the subnet/CIDR representation of this range.
294     *
295     * @return \IPLib\Range\Subnet
296     */
297    public function asSubnet()
298    {
299        switch ($this->getAddressType()) {
300            case AddressType::T_IPv4:
301                return new Subnet($this->getStartAddress(), $this->getEndAddress(), 8 * (4 - $this->asterisksCount));
302            case AddressType::T_IPv6:
303                return new Subnet($this->getStartAddress(), $this->getEndAddress(), 16 * (8 - $this->asterisksCount));
304        }
305    }
306
307    /**
308     * {@inheritdoc}
309     *
310     * @see \IPLib\Range\RangeInterface::getSubnetMask()
311     */
312    public function getSubnetMask()
313    {
314        if ($this->getAddressType() !== AddressType::T_IPv4) {
315            return null;
316        }
317        switch ($this->asterisksCount) {
318            case 0:
319                $bytes = array(255, 255, 255, 255);
320                break;
321            case 4:
322                $bytes = array(0, 0, 0, 0);
323                break;
324            default:
325                $bytes = array_pad(array_fill(0, 4 - $this->asterisksCount, 255), 4, 0);
326                break;
327        }
328
329        return IPv4::fromBytes($bytes);
330    }
331}
332