Passed
Push — master ( dbb22f...476838 )
by Matthieu
04:08
created

Address   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 69
dl 0
loc 405
ccs 77
cts 77
cp 1
rs 9.36
c 0
b 0
f 0
wmc 38

17 Methods

Rating   Name   Duplication   Size   Complexity  
A isRFC1918() 0 14 3
A __toString() 0 3 1
A asDotQuad() 0 8 1
A next() 0 3 1
A __construct() 0 8 3
A asInteger() 0 3 1
A fromString() 0 3 1
A isPrivate() 0 3 2
A shift() 0 3 1
A match() 0 3 1
A previous() 0 3 1
A isMulticast() 0 3 1
A isRFC6598() 0 4 2
B fromArray() 0 44 11
A int() 0 3 1
A fromInteger() 0 3 1
A getClass() 0 27 6
1
<?php
2
/**
3
 * Copyright (c) 2018 Mattheu Racine
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace mracine\IPTools\IPv4;
10
11
use OutOfBoundsException;
12
use InvalidArgumentException;
13
use DomainException;
14
use RuntimeException;
15
16
use mracine\IPTools\IPVersion;
17
18
/**
19
 * Represents an IPv4 address
20
 *
21
 * Used to represents and manipulate IPv4 addresses  
22
 * 
23
 * IMMUTABLE : once instancied, cannot be modified.
24
 * New instances are created and returned when results implies differents values. 
25
 *
26
 * @package IPTools
27
 */
28
class Address implements IPVersion
29
{
30
    /**
31
     * Classes of an address
32
     *
33
     * Obsolete for pure routing but still significant for multicat addresses (class D)
34
     * 
35
     * @see https://en.wikipedia.org/wiki/Classful_network 
36
     */ 
37
    const CLASS_A = "A",
38
          CLASS_B = "B",
39
          CLASS_C = "C",
40
          CLASS_D = "D",
41
          CLASS_E = "E";
42
43
    /**
44
     * IPv4 addresses are stored as a 32 bits integer
45
     *
46
     * @var int
47
     */
48
    protected $address = 0;
49
50
    // Implemnents version(); 
51
    use IPv4;
52
    
53
    /**
54
     * Creates an instance of Adress from an integer (0x00000000 to 0xffffffff)
55
     *
56
     * @see Address::fromInteger
57
     * @see Address::fromString
58
     * @see Address::fromCidr
59
     * @see Address::fromArray
60
     * @param int $address 32 bits integer reprsenting the adress
61
     * @throws OutOfBoundsException when the value is negative or greater than 32 bits value
62
     * @return self
63
     */
64 9
    public function __construct(int $address)
65
    {
66 9
        if ($address<0 || $address>0xffffffff)
67
        {
68 4
            throw new OutOfBoundsException(sprintf("Cannot convert 0x%x to an IPv4 address", $address));            
69
        } 
70
71 5
        $this->address =  $address;
72 5
    }
73
74
    /**
75
     * Creates an instance of Adress from an integer (0x00000000 to 0xffffffff)
76
     *
77
     * Maintained to avoid BC-break
78
     *
79
     * @param int $address 32 bits integer reprsenting the adress
80
     * @throws OutOfBoundsException when the value is negative or greater than 32 bits value
81
     * @return self
82
     */
83
84 3
    public static function fromInteger(int $address)
85
    {
86 3
        return new static($address);
87
    }
88
89
90
    /**
91
     * Creates an instance of Address from an array.
92
     *
93
     * The array must contains excly 4 elements (strings or integer).
94
     * Each elemnt represents a digit (byte) of the dotted quad notation ([192,168,1,10])
95
     *
96
     * @param (int|string)[] $address
97
     * @throws InvalidArgumentException when the array doesn not contains four elements
98
     * @throws InvalidArgumentException when a string element is empty 
99
     * @throws InvalidArgumentException when a string element is not a pure integer representation 
100
     * @throws InvalidArgumentException when an element is not a string nor an integer 
101
     * @throws OutOfBoundsException when a string element represent a negative value
102
     * @throws OutOfBoundsException when an element is negative or greater than 32 bits integer
103
     * @return self
104
     */
105
106 22
    public static function fromArray(Array $address)
107
    {
108 22
        if( count($address) != 4)
109
        {
110 5
            throw new InvalidArgumentException(sprintf("Array must contain 4 digits, %d found", count($address)));
111
        }
112
113 17
        $buffer = 0;
114 17
        foreach($address as $digit)
115
        {
116 17
            if (is_string($digit) )
117
            {        
118 6
                $digit = trim($digit);
119 6
                if (strlen($digit)==0)
120
                {
121 1
                    throw new InvalidArgumentException(sprintf("The array must contains only integers or strings with integer content , string found with empty value"));
122
                }
123 5
                if ( $digit[0] == '-' && ctype_digit(ltrim($digit, '-')) )
124
                {
125
                    // string type : "-123"
126 1
                    throw new OutOfBoundsException(sprintf("Cannot convert %d to Ipv4 addresss digit", $digit));
127
                }
128 4
                if (!ctype_digit($digit))
129
                {
130 1
                    throw new InvalidArgumentException(sprintf("The array must contains only integers or strings with integer content , string found with bad value %s", $digit));
131
                }
132
                // Here the string contains only numbers, can be casted to int safely
133 3
                $digit = (int)$digit;
134
            }
135
            
136 17
            if (!is_integer($digit) )
137
            {
138 1
                throw new InvalidArgumentException(sprintf("The array must contains only integers or strings with integer content , %s found", gettype($digit)));
139
            }
140
141 17
            if($digit<0 || $digit>0xff)
142
            {
143 5
                throw new OutOfBoundsException(sprintf("Cannot convert %d to Ipv4 addresss digit", $digit));
144
145
            }
146
            // shift adress left from one byte (8 bits), then add digit (the last byte)
147 16
            $buffer = ($buffer << 8) | $digit;
148
        }
149 8
        return static::fromInteger($buffer);
150
    }
151
152
    /**
153
     * Creates an instance of Address from a dotted quad formated string ("#.#.#.#"")
154
     *
155
     * @param string $address
156
     * @throws InvalidFormatException
157
     * @return self
158
     */
159 22
    public static function fromString(string $address)
160
    {
161 22
        return static::fromArray(explode('.', $address));
162
    }
163
164
    /**
165
     * Get the default 'dotted-quad' representation of the IPv4 address ("#.#.#.#")
166
     *
167
     * @return string
168
     */
169 2
    public function asDotQuad()
170
    {
171 2
        return sprintf(
172 2
            "%u.%u.%u.%u",
173 2
            (($this->address & (0xff << 24)) >> 24),
174 2
            (($this->address & (0xff << 16)) >> 16),
175 2
            (($this->address & (0xff << 8)) >> 8),
176 2
            ($this->address & 0xff)
177
        );
178
    }
179
180
    /**
181
     * Get the integer value of the IPv4 address between 0 to 0xffffffff
182
     *
183
     * @return int
184
     */
185
186 5
    public function asInteger()
187
    {
188 5
        return $this->address;
189
    }
190
191
    /**
192
     * Get the integer value of the IPv4 address between 0 to 0xffffffff
193
     *
194
     * Just a helper/short way to call asInteger()
195
     *
196
     * @see Address::asInteger()
197
     * @return int
198
     */
199
200 5
    public function int()
201
    {
202 5
        return $this->asInteger();
203
    }
204
205
    /**
206
     * Get the CIDR integer value (0-32) from the adress if possible (eg : "255.255.255.0" returns 24)
207
     *
208
     * @see Address::fromCidr()
209
     * @throws DomainException when the address cannot be converted to CIDR
210
     * @return integer A value beteween 0 to 32  
211
     */
212
    // public function asCidr()
213
    // {
214
    //     // Pas très élégant.... 
215
    //     for ($cidr=32 ; $cidr>=0 ; $cidr--)
216
    //     {
217
    //         $n = (0xffffffff << (32 - $cidr)) & 0xffffffff;
218
    //         if( $n == $this->address )
219
    //         {
220
    //             return $cidr;
221
    //         }
222
    //     }
223
    //     throw new DomainException(sprintf("Cannot convert address %s to CIDR, not a netmask", (string)$this));
224
    // }
225
226
227
    /**
228
     * Get a string representation of address
229
     *
230
     * Automagically called when PHP needs to convert an address to a string.
231
     * I choose to return the default 'dotted-quad' representation of the IPv4 address ("#.#.#.#")
232
     *
233
     * @return string
234
     */
235 2
    public function __toString()
236
    {
237 2
        return $this->asDotQuad();
238
    }
239
240
    /**
241
     * Tests if two addresses are equivalent (same value)
242
     *
243
     * @param self $address
244
     * @return bool
245
     */
246 7
     public function match(Address $address)
247
    {
248 7
        return $this->int() == $address->int();
249
    }
250
251
    /**
252
     * Get the class of the IP address (for obsolete classfull routing)
253
     *
254
     * @return string a constant CLASS_[A-E]
255
     */
256 13
    public function getClass()
257
    {
258 13
        $higherOctet = $this->address >> 24;
259
260 13
        if (($higherOctet & 0x80) == 0) {
261 5
            return self::CLASS_A;
262
        }
263
264 8
        if (($higherOctet & 0xC0) == 0x80) {
265 2
            return self::CLASS_B;
266
        }
267
268 6
        if (($higherOctet & 0xE0) == 0xC0) {
269 2
            return self::CLASS_C;
270
        }
271
272 4
        if (($higherOctet & 0xF0) == 0xE0) {
273 2
            return self::CLASS_D;
274
        }
275
276 2
        if (($higherOctet & 0xF0) == 0xF0) {
277 2
            return self::CLASS_E;
278
        }
279
280
        // Should never be triggered
281
        // @codeCoverageIgnoreStart
282
        throw new RuntimeException();
283
        // @codeCoverageIgnoreEnd
284
    }
285
286
    /**
287
     * Returns whether the address is part of multicast address range
288
     * 
289
     * @return bool 
290
     */
291 8
    public function isMulticast()
292
    {
293 8
        return $this->getClass() === self::CLASS_D;
294
    }
295
296
    /**
297
     * Get the IP version (IPv4 or IPv6) of this address instance
298
     * 
299
     * @return int a constant IPv4 or IPv6
300
     */
301
    // public function version()
302
    // {
303
    //     return self::IPv4;
304
    // }
305
306
    /**
307
     * Tells if the adresse can be used as a netmask
308
     * 
309
     * A netmask is a kind of address.
310
     * Only a little nuber of adresses can be used as netmasks (33).
311
     * Only adresses with left bits sets to 1 and rights bits sets to 0, with no mix between this two zones are eligible to be netmasks.
312
     * NOTE : An adress can be a netmask if it can be converted to a CIDR value and vice versa
313
     *
314
     * @see Address::fromCidr()
315
     * @return bool  
316
     */
317
    // public function isNetmask()
318
    // {
319
    //     // Pas très élégant non plus.... 
320
    //     try
321
    //     {
322
    //         $this->asCidr();
323
    //     }
324
    //     catch(DomainException $e)
325
    //     {
326
    //         return false;
327
    //     }
328
    //     return true;
329
    // }
330
331
    /**
332
     * Returns whether the address is part of the subnets defined in RFC 1918
333
     *
334
     * RFC1918 defines privates, non Internet routables subnets for end users's privates network
335
     * This subnets are :
336
     *  - 10.0.0.0/8
337
     *  - 172.16.0.0/12
338
     *  - 192.168.0.0/16
339
     * This subnets should be used by end users to build their privates networks.
340
     *
341
     * @see https://tools.ietf.org/html/rfc1918
342
     * @return bool
343
     */
344 14
    public function isRFC1918()
345
    {
346
        /** @var $subnets Subnet[] */
347
        $subnets = array(
348 14
            Subnet::fromCidr(Address::fromString("10.0.0.0"), 8),
349 14
            Subnet::fromCidr(Address::fromString("172.16.0.0"), 12),
350 14
            Subnet::fromCidr(Address::fromString("192.168.0.0"), 16),
351
        );
352 14
        foreach($subnets as $subnet) {
353 14
            if($subnet->contains($this)) {
354 14
                return true;
355
            }
356
        }
357 11
        return false;
358
    }
359
360
    /**
361
     * Returns whether the address is part of the subnets defined in RFC 6598
362
     *
363
     * RFC6598 defines a private, non Internet routable subnet for Carrir Grade Networks, mainly used by FAI or non end users network providers 
364
     * This subnet is 100.64.0.0/10
365
     * This subnet should not be used by end users
366
     *
367
     * @see https://tools.ietf.org/html/rfc6598
368
     * @return bool
369
     */
370 14
    public function isRFC6598()
371
    {
372 14
        $subnet = Subnet::fromCidr(Address::fromString("100.64.0.0"), 10);
373 14
        return $subnet->contains($this) ? true : false;
374
    }
375
376
    /**
377
     * Returns whether the address is part of non Internet routable subnets
378
     * 
379
     * @todo improve: multicast, other on routables, ... ?
380
     * @return bool 
381
     */
382 14
    public function isPrivate()
383
    {
384 14
        return ($this->isRFC1918()  || $this->isRFC6598());
385
    }
386
387
    /**
388
     * Returns an address shifted by an amount of units 
389
     *
390
     * NOTICE : A new instance of address is instanciated, does not shitf the instance used.
391
     *
392
     * @param int $offset 
393
     * @throws OutOfBoundsException when the resulting address is out of the bounds (negative or greater than 32 bits value)
394
     * @return self
395
     */
396 14
    public function shift(int $offset)
397
    {
398 14
        return new static($this->address+$offset);
399
    }
400
401
    /**
402
     * Returns the address immediately following this address 
403
     *
404
     * Examples :
405
     *   - "1.2.3.4" => "1.2.3.5"
406
     *   - "1.2.3.255" => "1.2.4.0"
407
     *
408
     *  NOTICE : A new instance of address is instanciated, does not modify the instance used.
409
     *
410
     * @throws OutOfBoundsException when the resulting address is out of the bounds (more than 32 bits value). Happend only if try to get next of "255.255.255.255"
411
     * @return self
412
     */
413 11
    public function next()
414
    {
415 11
        return $this->shift(1);
416
    }
417
418
    /**
419
     * Returns the address immediately preceding this address 
420
     *
421
     * Examples :
422
     *   - "1.2.3.5" => "1.2.3.4"
423
     *   - "1.2.4.0" => "1.2.3.255"
424
     *
425
     * NOTICE : A new instance of address is instanciated, does not modify the instance used.
426
     *
427
     * @throws OutOfBoundsException when the resulting address is out of the bounds (less than 0). Happend only if try to get previous of "0.0.0.0"
428
     * @return self
429
     */
430 11
    public function previous()
431
    {
432 11
        return $this->shift(-1);
433
    }
434
}