Completed
Push — master ( fe5fd9...28190b )
by Sam
07:00 queued 04:19
created

Parser::populateWithLastStated()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.016

Importance

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