Passed
Push — master ( c1e68f...f0bd78 )
by Sam
01:33 queued 10s
created

Parser::extractComment()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 3
nop 1
dl 0
loc 22
ccs 13
cts 13
cp 1
crap 3
rs 9.8666
c 0
b 0
f 0
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
     * @param array $rdataHandlers
67
     */
68 25
    public function __construct(array $rdataHandlers = [])
69
    {
70 25
        $this->rdataHandlers = array_merge(
71 25
            [PTR::TYPE => [$this, 'ptrHandler']],
72 25
            $rdataHandlers
73
        );
74 25
    }
75
76
    /**
77
     * @param string $name
78
     * @param string $zone
79
     * @param int    $commentOptions
80
     *
81
     * @return Zone
82
     *
83
     * @throws ParseException
84
     */
85 24
    public static function parse(string $name, string $zone, int $commentOptions = Comments::NONE): Zone
86
    {
87 24
        return (new self())->makeZone($name, $zone, $commentOptions);
88
    }
89
90
    /**
91
     * @param string $name
92
     * @param string $string
93
     * @param int    $commentOptions
94
     *
95
     * @return Zone
96
     *
97
     * @throws ParseException
98
     */
99 25
    public function makeZone(string $name, string $string, int $commentOptions = Comments::NONE): Zone
100
    {
101 25
        $this->zone = new Zone($name);
102 25
        $this->string = Normaliser::normalise($string, $commentOptions);
103
104 25
        foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
105 25
            $this->processLine($line);
106
        }
107
108 22
        return $this->zone;
109
    }
110
111
    /**
112
     * @param string $line
113
     *
114
     * @throws ParseException
115
     */
116 25
    private function processLine(string $line): void
117
    {
118 25
        list($entry, $comment) = $this->extractComment($line);
119
120 25
        $this->currentResourceRecord = new ResourceRecord();
121 25
        $this->currentResourceRecord->setTtl($this->zone->getDefaultTtl());
122 25
        $this->currentResourceRecord->setComment($comment);
123
124 25
        if ('' === $entry) {
125 2
            $this->zone->addResourceRecord($this->currentResourceRecord);
126
127 2
            return;
128
        }
129
130 25
        $iterator = new ResourceRecordIterator($entry);
131
132 25
        if ($this->isControlEntry($iterator)) {
133 10
            $this->processControlEntry($iterator);
134
135 10
            return;
136
        }
137
138 25
        $this->processEntry($iterator);
139 22
        $this->zone->addResourceRecord($this->currentResourceRecord);
140 22
    }
141
142
    /**
143
     * @param ResourceRecordIterator $iterator
144
     *
145
     * @throws ParseException
146
     */
147 25
    private function processEntry(ResourceRecordIterator $iterator): void
148
    {
149 25
        if ($this->isTTL($iterator)) {
150 14
            $this->currentResourceRecord->setTtl((int) $iterator->current());
151 14
            $iterator->next();
152 14
            $this->processEntry($iterator);
153
154 12
            return;
155
        }
156
157 25
        if ($this->isClass($iterator)) {
158 23
            $this->currentResourceRecord->setClass(strtoupper($iterator->current()));
159 23
            $iterator->next();
160 23
            $this->processEntry($iterator);
161
162 21
            return;
163
        }
164
165 25
        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...
166 24
            $this->currentResourceRecord->setName($iterator->current());
167 24
            $iterator->next();
168 24
            $this->processEntry($iterator);
169
170 22
            return;
171
        }
172
173 25
        if ($this->isType($iterator)) {
174 24
            $this->currentResourceRecord->setRdata($this->extractRdata($iterator));
175 22
            $this->populateWithLastStated();
176
177 22
            return;
178
        }
179
180 1
        throw new ParseException(sprintf('Could not parse entry "%s".', implode(' ', $iterator->getArrayCopy())));
181
    }
182
183
    /**
184
     * If no domain-name, TTL, or class is set on the record, populate object with last stated value.
185
     *
186
     * @see https://www.ietf.org/rfc/rfc1035 Section 5.1
187
     */
188 22
    private function populateWithLastStated(): void
189
    {
190 22
        if (null === $this->currentResourceRecord->getName()) {
0 ignored issues
show
introduced by
The condition null === $this->currentResourceRecord->getName() is always false.
Loading history...
191 3
            $this->currentResourceRecord->setName($this->lastStatedDomain);
192
        } else {
193 22
            $this->lastStatedDomain = $this->currentResourceRecord->getName();
194
        }
195
196 22
        if (null === $this->currentResourceRecord->getTtl()) {
197 6
            $this->currentResourceRecord->setTtl($this->lastStatedTtl);
198
        } else {
199 18
            $this->lastStatedTtl = $this->currentResourceRecord->getTtl();
200
        }
201
202 22
        if (null === $this->currentResourceRecord->getClass()) {
203
            $this->currentResourceRecord->setClass($this->lastStatedClass);
204
        } else {
205 22
            $this->lastStatedClass = $this->currentResourceRecord->getClass();
206
        }
207 22
    }
208
209
    /**
210
     * Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
211
     *
212
     * @param ResourceRecordIterator $iterator
213
     */
214 10
    private function processControlEntry(ResourceRecordIterator $iterator): void
215
    {
216 10
        if ('$TTL' === strtoupper($iterator->current())) {
217 9
            $iterator->next();
218 9
            $this->zone->setDefaultTtl((int) $iterator->current());
219
        }
220
221 10
        if ('$ORIGIN' === strtoupper($iterator->current())) {
222 10
            $iterator->next();
223 10
            $this->zone->setName((string) $iterator->current());
224
        }
225 10
    }
226
227
    /**
228
     * Determine if iterant is a resource name.
229
     *
230
     * @param ResourceRecordIterator $iterator
231
     *
232
     * @return bool
233
     */
234 25
    private function isResourceName(ResourceRecordIterator $iterator): bool
235
    {
236
        // Look ahead and determine if the next token is a TTL, Class, or valid Type.
237 25
        $iterator->next();
238
239 25
        if (!$iterator->valid()) {
240
            return false;
241
        }
242
243 25
        $isName = $this->isTTL($iterator) ||
244 25
            $this->isClass($iterator, 'DOMAIN') ||
245 25
            $this->isType($iterator);
246 25
        $iterator->prev();
247
248 25
        if (!$isName) {
249 24
            return false;
250
        }
251
252 24
        if (0 === $iterator->key()) {
253 24
            return true;
254
        }
255
256 3
        return false;
257
    }
258
259
    /**
260
     * Determine if iterant is a class.
261
     *
262
     * @param ResourceRecordIterator $iterator
263
     * @param string|null            $origin   the previously assumed resource record parameter, either 'TTL' or NULL
264
     *
265
     * @return bool
266
     */
267 25
    private function isClass(ResourceRecordIterator $iterator, $origin = null): bool
268
    {
269 25
        if (!Classes::isValid($iterator->current())) {
270 25
            return false;
271
        }
272
273 24
        $iterator->next();
274 24
        if ('TTL' === $origin) {
275 11
            $isClass = $this->isType($iterator);
276
        } else {
277 23
            $isClass = $this->isTTL($iterator, 'CLASS') || $this->isType($iterator);
278
        }
279 24
        $iterator->prev();
280
281 24
        return $isClass;
282
    }
283
284
    /**
285
     * Determine if current iterant is an Rdata type string.
286
     *
287
     * @param ResourceRecordIterator $iterator
288
     *
289
     * @return bool
290
     */
291 25
    private function isType(ResourceRecordIterator $iterator): bool
292
    {
293 25
        return Types::isValid(strtoupper($iterator->current())) || array_key_exists($iterator->current(), $this->rdataHandlers);
294
    }
295
296
    /**
297
     * Determine if iterant is a control entry such as $TTL, $ORIGIN, $INCLUDE, etcetera.
298
     *
299
     * @param ResourceRecordIterator $iterator
300
     *
301
     * @return bool
302
     */
303 25
    private function isControlEntry(ResourceRecordIterator $iterator): bool
304
    {
305 25
        return 1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current());
306
    }
307
308
    /**
309
     * Determine if the iterant is a TTL (i.e. it is an integer).
310
     *
311
     * @param ResourceRecordIterator $iterator
312
     * @param string                 $origin   the previously assumed resource record parameter, either 'CLASS' or NULL
313
     *
314
     * @return bool
315
     */
316 25
    private function isTTL(ResourceRecordIterator $iterator, $origin = null): bool
317
    {
318 25
        if (1 !== preg_match('/^\d+$/', $iterator->current())) {
319 25
            return false;
320
        }
321
322 23
        $iterator->next();
323 23
        if ('CLASS' === $origin) {
324 5
            $isTtl = $this->isType($iterator);
325
        } else {
326 23
            $isTtl = $this->isClass($iterator, 'TTL') || $this->isType($iterator);
327
        }
328 23
        $iterator->prev();
329
330 23
        return $isTtl;
331
    }
332
333
    /**
334
     * Split a DNS zone line into a resource record and a comment.
335
     *
336
     * @param string $rr
337
     *
338
     * @return array [$entry, $comment]
339
     */
340 25
    private function extractComment(string $rr): array
341
    {
342 25
        $string = new StringIterator($rr);
343 25
        $entry = '';
344 25
        $comment = null;
345
346 25
        while ($string->valid()) {
347
            //If a semicolon is within double quotes, it will not be treated as the beginning of a comment.
348 25
            $entry .= $this->extractDoubleQuotedText($string);
349
350 25
            if ($string->is(Tokens::SEMICOLON)) {
351 4
                $string->next();
352 4
                $comment = $string->getRemainingAsString();
353
354 4
                break;
355
            }
356
357 25
            $entry .= $string->current();
358 25
            $string->next();
359
        }
360
361 25
        return [$entry, $comment];
362
    }
363
364
    /**
365
     * Extract text within double quotation context.
366
     *
367
     * @param StringIterator $string
368
     *
369
     * @return string
370
     */
371 25
    private function extractDoubleQuotedText(StringIterator $string): string
372
    {
373 25
        if ($string->isNot(Tokens::DOUBLE_QUOTES)) {
374 25
            return '';
375
        }
376
377 12
        $entry = $string->current();
378 12
        $string->next();
379
380 12
        while ($string->isNot(Tokens::DOUBLE_QUOTES)) {
381
            //If the current char is a backslash, treat the next char as being escaped.
382 12
            if ($string->is(Tokens::BACKSLASH)) {
383 6
                $entry .= $string->current();
384 6
                $string->next();
385
            }
386 12
            $entry .= $string->current();
387 12
            $string->next();
388
        }
389
390 12
        return $entry;
391
    }
392
393
    /**
394
     * @param ResourceRecordIterator $iterator
395
     *
396
     * @return RdataInterface
397
     *
398
     * @throws ParseException
399
     */
400 24
    private function extractRdata(ResourceRecordIterator $iterator): RdataInterface
401
    {
402 24
        $type = strtoupper($iterator->current());
403 24
        $iterator->next();
404
405 24
        if (array_key_exists($type, $this->rdataHandlers)) {
406 4
            return call_user_func($this->rdataHandlers[$type], $iterator);
407
        }
408
409
        try {
410 22
            return Factory::textToRdataType($type, $iterator->getRemainingAsString());
411 2
        } catch (\Exception $exception) {
412 2
            throw new ParseException(sprintf('Could not extract Rdata from resource record "%s".', (string) $iterator), null, $exception);
413
        }
414
    }
415
416
    /**
417
     * This handler addresses the special case where an integer resource name could be confused for a TTL, for instance:
418
     * 50 IN PTR mx1.acme.com.
419
     *
420
     * In the above, if the integer is below 256 then it is assumed to represent an octet of an IPv4 address.
421
     *
422
     * @param ResourceRecordIterator $iterator
423
     *
424
     * @return PTR
425
     */
426 3
    private function ptrHandler(ResourceRecordIterator $iterator): PTR
427
    {
428 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...
429 1
            if ($this->currentResourceRecord->getTtl() < 256) {
430 1
                $this->currentResourceRecord->setName((string) $this->currentResourceRecord->getTtl());
431 1
                $this->currentResourceRecord->setTtl(null);
432
            }
433
        }
434
435
        /** @var PTR $ptr */
436 3
        $ptr = PTR::fromText($iterator->getRemainingAsString());
437
438 3
        return $ptr;
439
    }
440
}
441