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