Passed
Branch master (8940db)
by Sam
02:38
created

Parser::processControlEntry()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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