Completed
Push — master ( 1a3f9c...9e9273 )
by Sam
09:05 queued 06:12
created

Parser::ptrHandler()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.8437

Importance

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