Completed
Push — master ( ff126e...480caa )
by Sam
12s queued 11s
created

Parser::extractComment()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 8

Importance

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