Completed
Branch scrutinizer-changes (af95a9)
by Sam
05:03
created

Parser::isControlEntry()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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