Completed
Push — master ( 28190b...82ff4e )
by Sam
07:35 queued 04:25
created

Parser   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 366
Duplicated Lines 0 %

Test Coverage

Coverage 96.53%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 139
c 3
b 0
f 0
dl 0
loc 366
ccs 139
cts 144
cp 0.9653
rs 7.44
wmc 52

16 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 3 1
A makeZone() 0 10 2
A __construct() 0 5 1
A isClass() 0 15 4
A isTTL() 0 19 5
A ptrHandler() 0 13 4
A isControlEntry() 0 3 1
A extractDoubleQuotedText() 0 20 4
A extractRdata() 0 13 3
B processEntry() 0 34 6
A isResourceName() 0 23 6
A isType() 0 3 2
A populateWithLastStated() 0 18 4
A processControlEntry() 0 10 3
A processLine() 0 23 3
A extractComment() 0 22 3

How to fix   Complexity   

Complex Class

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Badcow DNS Library.
7
 *
8
 * (c) Samuel Williams <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Badcow\DNS\Parser;
15
16
use Badcow\DNS\Classes;
17
use Badcow\DNS\Rdata\Factory;
18
use Badcow\DNS\Rdata\PTR;
19
use Badcow\DNS\Rdata\RdataInterface;
20
use Badcow\DNS\Rdata\Types;
21
use Badcow\DNS\ResourceRecord;
22
use Badcow\DNS\Zone;
23
24
class Parser
25
{
26
    /**
27
     * @var string
28
     */
29
    private $string;
30
31
    /**
32
     * @var Zone
33
     */
34
    private $zone;
35
36
    /**
37
     * Array of methods that take an ArrayIterator and return an Rdata object. The array key is the Rdata type.
38
     *
39
     * @var callable[]
40
     */
41
    private $rdataHandlers = [];
42
43
    /**
44
     * @var ResourceRecord
45
     */
46
    private $currentResourceRecord;
47
48
    /**
49
     * @var string
50
     */
51
    private $lastStatedDomain;
52
53
    /**
54
     * @var int
55
     */
56
    private $lastStatedTtl;
57
58
    /**
59
     * @var string
60
     */
61
    private $lastStatedClass;
62
63
    /**
64
     * Parser constructor.
65
     */
66 26
    public function __construct(array $rdataHandlers = [])
67
    {
68 26
        $this->rdataHandlers = array_merge(
69 26
            [PTR::TYPE => [$this, 'ptrHandler']],
70
            $rdataHandlers
71
        );
72 26
    }
73
74
    /**
75
     * @throws ParseException
76
     */
77 25
    public static function parse(string $name, string $zone, int $commentOptions = Comments::NONE): Zone
78
    {
79 25
        return (new self())->makeZone($name, $zone, $commentOptions);
80
    }
81
82
    /**
83
     * @throws ParseException
84
     */
85 26
    public function makeZone(string $name, string $string, int $commentOptions = Comments::NONE): Zone
86
    {
87 26
        $this->zone = new Zone($name);
88 26
        $this->string = Normaliser::normalise($string, $commentOptions);
89
90 26
        foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
91 26
            $this->processLine($line);
92
        }
93
94 23
        return $this->zone;
95
    }
96
97
    /**
98
     * @throws ParseException
99
     */
100 26
    private function processLine(string $line): void
101
    {
102 26
        list($entry, $comment) = $this->extractComment($line);
103
104 26
        $this->currentResourceRecord = new ResourceRecord();
105 26
        $this->currentResourceRecord->setComment($comment);
106
107 26
        if ('' === $entry) {
108 2
            $this->zone->addResourceRecord($this->currentResourceRecord);
109
110 2
            return;
111
        }
112
113 26
        $iterator = new ResourceRecordIterator($entry);
114
115 26
        if ($this->isControlEntry($iterator)) {
116 11
            $this->processControlEntry($iterator);
117
118 11
            return;
119
        }
120
121 26
        $this->processEntry($iterator);
122 23
        $this->zone->addResourceRecord($this->currentResourceRecord);
123 23
    }
124
125
    /**
126
     * @throws ParseException
127
     */
128 26
    private function processEntry(ResourceRecordIterator $iterator): void
129
    {
130 26
        if ($this->isTTL($iterator)) {
131 15
            $this->currentResourceRecord->setTtl(TimeFormat::toSeconds($iterator->current()));
132 15
            $iterator->next();
133 15
            $this->processEntry($iterator);
134
135 13
            return;
136
        }
137
138 26
        if ($this->isClass($iterator)) {
139 24
            $this->currentResourceRecord->setClass(strtoupper($iterator->current()));
140 24
            $iterator->next();
141 24
            $this->processEntry($iterator);
142
143 22
            return;
144
        }
145
146 26
        if ($this->isResourceName($iterator) && null === $this->currentResourceRecord->getName()) {
0 ignored issues
show
introduced by
The condition null === $this->currentResourceRecord->getName() is always false.
Loading history...
147 25
            $this->currentResourceRecord->setName($iterator->current());
148 25
            $iterator->next();
149 25
            $this->processEntry($iterator);
150
151 23
            return;
152
        }
153
154 26
        if ($this->isType($iterator)) {
155 25
            $this->currentResourceRecord->setRdata($this->extractRdata($iterator));
156 23
            $this->populateWithLastStated();
157
158 23
            return;
159
        }
160
161 1
        throw new ParseException(sprintf('Could not parse entry "%s".', implode(' ', $iterator->getArrayCopy())));
162
    }
163
164
    /**
165
     * If no domain-name, TTL, or class is set on the record, populate object with last stated value.
166
     *
167
     * @see https://www.ietf.org/rfc/rfc1035 Section 5.1
168
     */
169 23
    private function populateWithLastStated(): void
170
    {
171 23
        if (empty($this->currentResourceRecord->getName())) {
172 3
            $this->currentResourceRecord->setName($this->lastStatedDomain);
173
        } else {
174 23
            $this->lastStatedDomain = $this->currentResourceRecord->getName();
175
        }
176
177 23
        if (null === $this->currentResourceRecord->getTtl()) {
178 13
            $this->currentResourceRecord->setTtl($this->lastStatedTtl ?? $this->zone->getDefaultTTl());
179
        } else {
180 13
            $this->lastStatedTtl = $this->currentResourceRecord->getTtl();
181
        }
182
183 23
        if (null === $this->currentResourceRecord->getClass()) {
184
            $this->currentResourceRecord->setClass($this->lastStatedClass);
185
        } else {
186 23
            $this->lastStatedClass = $this->currentResourceRecord->getClass();
187
        }
188 23
    }
189
190
    /**
191
     * Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
192
     */
193 11
    private function processControlEntry(ResourceRecordIterator $iterator): void
194
    {
195 11
        if ('$TTL' === strtoupper($iterator->current())) {
196 10
            $iterator->next();
197 10
            $this->zone->setDefaultTtl(TimeFormat::toSeconds($iterator->current()));
198
        }
199
200 11
        if ('$ORIGIN' === strtoupper($iterator->current())) {
201 10
            $iterator->next();
202 10
            $this->zone->setName((string) $iterator->current());
203
        }
204 11
    }
205
206
    /**
207
     * Determine if iterant is a resource name.
208
     */
209 26
    private function isResourceName(ResourceRecordIterator $iterator): bool
210
    {
211
        // Look ahead and determine if the next token is a TTL, Class, or valid Type.
212 26
        $iterator->next();
213
214 26
        if (!$iterator->valid()) {
215
            return false;
216
        }
217
218 26
        $isName = $this->isTTL($iterator) ||
219 26
            $this->isClass($iterator, 'DOMAIN') ||
220 26
            $this->isType($iterator);
221 26
        $iterator->prev();
222
223 26
        if (!$isName) {
224 25
            return false;
225
        }
226
227 25
        if (0 === $iterator->key()) {
228 25
            return true;
229
        }
230
231 3
        return false;
232
    }
233
234
    /**
235
     * Determine if iterant is a class.
236
     *
237
     * @param string|null $origin the previously assumed resource record parameter, either 'TTL' or NULL
238
     */
239 26
    private function isClass(ResourceRecordIterator $iterator, $origin = null): bool
240
    {
241 26
        if (!Classes::isValid($iterator->current())) {
242 26
            return false;
243
        }
244
245 25
        $iterator->next();
246 25
        if ('TTL' === $origin) {
247 12
            $isClass = $this->isType($iterator);
248
        } else {
249 24
            $isClass = $this->isTTL($iterator, 'CLASS') || $this->isType($iterator);
250
        }
251 25
        $iterator->prev();
252
253 25
        return $isClass;
254
    }
255
256
    /**
257
     * Determine if current iterant is an Rdata type string.
258
     */
259 26
    private function isType(ResourceRecordIterator $iterator): bool
260
    {
261 26
        return Types::isValid(strtoupper($iterator->current())) || array_key_exists($iterator->current(), $this->rdataHandlers);
262
    }
263
264
    /**
265
     * Determine if iterant is a control entry such as $TTL, $ORIGIN, $INCLUDE, etcetera.
266
     */
267 26
    private function isControlEntry(ResourceRecordIterator $iterator): bool
268
    {
269 26
        return 1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current());
270
    }
271
272
    /**
273
     * Determine if the iterant is a TTL (i.e. it is an integer after domain-name).
274
     *
275
     * @param string $origin the previously assumed resource record parameter, either 'CLASS' or NULL
276
     */
277 26
    private function isTTL(ResourceRecordIterator $iterator, $origin = null): bool
278
    {
279 26
        if (!TimeFormat::isTimeFormat($iterator->current())) {
280 26
            return false;
281
        }
282
283 24
        if ($iterator->key() < 1) {
284 5
            return false;
285
        }
286
        
287 24
        $iterator->next();
288 24
        if ('CLASS' === $origin) {
289 5
            $isTtl = $this->isType($iterator);
290
        } else {
291 24
            $isTtl = $this->isClass($iterator, 'TTL') || $this->isType($iterator);
292
        }
293 24
        $iterator->prev();
294
295 24
        return $isTtl;
296
    }
297
298
    /**
299
     * Split a DNS zone line into a resource record and a comment.
300
     *
301
     * @return array [$entry, $comment]
302
     */
303 26
    private function extractComment(string $rr): array
304
    {
305 26
        $string = new StringIterator($rr);
306 26
        $entry = '';
307 26
        $comment = null;
308
309 26
        while ($string->valid()) {
310
            //If a semicolon is within double quotes, it will not be treated as the beginning of a comment.
311 26
            $entry .= $this->extractDoubleQuotedText($string);
312
313 26
            if ($string->is(Tokens::SEMICOLON)) {
314 4
                $string->next();
315 4
                $comment = $string->getRemainingAsString();
316
317 4
                break;
318
            }
319
320 26
            $entry .= $string->current();
321 26
            $string->next();
322
        }
323
324 26
        return [$entry, $comment];
325
    }
326
327
    /**
328
     * Extract text within double quotation context.
329
     */
330 26
    private function extractDoubleQuotedText(StringIterator $string): string
331
    {
332 26
        if ($string->isNot(Tokens::DOUBLE_QUOTES)) {
333 26
            return '';
334
        }
335
336 12
        $entry = $string->current();
337 12
        $string->next();
338
339 12
        while ($string->isNot(Tokens::DOUBLE_QUOTES)) {
340
            //If the current char is a backslash, treat the next char as being escaped.
341 12
            if ($string->is(Tokens::BACKSLASH)) {
342 6
                $entry .= $string->current();
343 6
                $string->next();
344
            }
345 12
            $entry .= $string->current();
346 12
            $string->next();
347
        }
348
349 12
        return $entry;
350
    }
351
352
    /**
353
     * @throws ParseException
354
     */
355 25
    private function extractRdata(ResourceRecordIterator $iterator): RdataInterface
356
    {
357 25
        $type = strtoupper($iterator->current());
358 25
        $iterator->next();
359
360 25
        if (array_key_exists($type, $this->rdataHandlers)) {
361 4
            return call_user_func($this->rdataHandlers[$type], $iterator);
362
        }
363
364
        try {
365 23
            return Factory::textToRdataType($type, $iterator->getRemainingAsString());
366 2
        } catch (\Exception $exception) {
367 2
            throw new ParseException(sprintf('Could not extract Rdata from resource record "%s".', (string) $iterator), null, $exception);
368
        }
369
    }
370
371
    /**
372
     * This handler addresses the special case where an integer resource name could be confused for a TTL, for instance:
373
     * 50 IN PTR mx1.acme.com.
374
     *
375
     * In the above, if the integer is below 256 then it is assumed to represent an octet of an IPv4 address.
376
     */
377 3
    private function ptrHandler(ResourceRecordIterator $iterator): PTR
378
    {
379 3
        if (null === $this->currentResourceRecord->getName() && null !== $this->currentResourceRecord->getTtl()) {
0 ignored issues
show
introduced by
The condition null === $this->currentResourceRecord->getName() is always false.
Loading history...
380
            if ($this->currentResourceRecord->getTtl() < 256) {
381
                $this->currentResourceRecord->setName((string) $this->currentResourceRecord->getTtl());
382
                $this->currentResourceRecord->setTtl(null);
383
            }
384
        }
385
386 3
        $ptr = new PTR();
387 3
        $ptr->fromText($iterator->getRemainingAsString());
388
389 3
        return $ptr;
390
    }
391
}
392