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