Completed
Pull Request — master (#41)
by Sam
01:41
created

Parser::ptrHandler()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.0342

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 8
cts 9
cp 0.8889
rs 9.4222
c 0
b 0
f 0
cc 5
nc 6
nop 1
crap 5.0342
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 14
    public function __construct(array $rdataHandlers = [])
64
    {
65 14
        $this->rdataHandlers = array_merge(
66 14
            RdataHandlers::getHandlers(),
67 14
            ['PTR' => __CLASS__.'::ptrHandler'],
68 14
            $rdataHandlers
69
        );
70 14
    }
71
72
    /**
73
     * @param string $name
74
     * @param string $zone
75
     *
76
     * @return Zone
77
     *
78
     * @throws ParseException
79
     */
80 13
    public static function parse(string $name, string $zone): Zone
81
    {
82 13
        return (new self())->makeZone($name, $zone);
83
    }
84
85
    /**
86
     * @param string $name
87
     * @param string $string
88
     *
89
     * @return Zone
90
     *
91
     * @throws ParseException
92
     */
93 14
    public function makeZone(string $name, string $string): Zone
94
    {
95 14
        $this->zone = new Zone($name);
96 14
        $this->string = Normaliser::normalise($string);
97
98 14
        foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
99 14
            $this->processLine($line);
100
        }
101
102 11
        return $this->zone;
103
    }
104
105
    /**
106
     * @param string $line
107
     *
108
     * @throws ParseException
109
     */
110 14
    private function processLine(string $line): void
111
    {
112 14
        $iterator = new ResourceRecordIterator($line);
113
114 14
        if ($this->isControlEntry($iterator)) {
115 7
            $this->processControlEntry($iterator);
116
117 7
            return;
118
        }
119
120 14
        $this->currentResourceRecord = new ResourceRecord();
121 14
        $this->processEntry($iterator);
122 11
        $this->zone->addResourceRecord($this->currentResourceRecord);
123 11
    }
124
125
    /**
126
     * @param ResourceRecordIterator $iterator
127
     *
128
     * @throws ParseException
129
     */
130 14
    private function processEntry(ResourceRecordIterator $iterator): void
131
    {
132 14
        if ($this->isTTL($iterator)) {
133 9
            $this->currentResourceRecord->setTtl((int) $iterator->current());
134 9
            $iterator->next();
135 9
            $this->processEntry($iterator);
136
137 7
            return;
138
        }
139
140 14
        if ($this->isClass($iterator)) {
141 11
            $this->currentResourceRecord->setClass(strtoupper($iterator->current()));
142 11
            $iterator->next();
143 11
            $this->processEntry($iterator);
144
145 9
            return;
146
        }
147
148 14
        if ($this->isResourceName($iterator) && null === $this->currentResourceRecord->getName()) {
149 13
            $this->currentResourceRecord->setName($iterator->current());
150 13
            $iterator->next();
151 13
            $this->processEntry($iterator);
152
153 11
            return;
154
        }
155
156 14
        if ($this->isType($iterator)) {
157 13
            $this->currentResourceRecord->setRdata($this->extractRdata($iterator));
158 11
            $this->populateWithLastStated();
159
160 11
            return;
161
        }
162
163 1
        throw new ParseException(sprintf('Could not parse entry "%s".', implode(' ', $iterator->getArrayCopy())));
164
    }
165
166
    /**
167
     * If no domain-name, TTL, or class is set on the record, populate object with last stated value.
168
     *
169
     * @see https://www.ietf.org/rfc/rfc1035 Section 5.1
170
     */
171 11
    private function populateWithLastStated(): void
172
    {
173 11
        if (null === $this->currentResourceRecord->getName()) {
174 2
            $this->currentResourceRecord->setName($this->lastStatedDomain);
175
        } else {
176 11
            $this->lastStatedDomain = $this->currentResourceRecord->getName();
177
        }
178
179 11
        if (null === $this->currentResourceRecord->getTtl()) {
180 7
            $this->currentResourceRecord->setTtl($this->lastStatedTtl);
181
        } else {
182 7
            $this->lastStatedTtl = $this->currentResourceRecord->getTtl();
183
        }
184
185 11
        if (null === $this->currentResourceRecord->getClass()) {
186 7
            $this->currentResourceRecord->setClass($this->lastStatedClass);
187
        } else {
188 9
            $this->lastStatedClass = $this->currentResourceRecord->getClass();
189
        }
190 11
    }
191
192
    /**
193
     * Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
194
     *
195
     * @param ResourceRecordIterator $iterator
196
     */
197 7
    private function processControlEntry(ResourceRecordIterator $iterator): void
198
    {
199 7
        if ('$TTL' === strtoupper($iterator->current())) {
200 7
            $iterator->next();
201 7
            $this->zone->setDefaultTtl((int) $iterator->current());
202
        }
203 7
    }
204
205
    /**
206
     * Determine if iterant is a resource name.
207
     *
208
     * @param ResourceRecordIterator $iterator
209
     *
210
     * @return bool
211
     */
212 14
    private function isResourceName(ResourceRecordIterator $iterator): bool
213
    {
214 14
        $iterator->next();
215
216 14
        if (!$iterator->valid()) {
217
            return false;
218
        }
219
220 14
        $isName = $this->isTTL($iterator) ||
221 14
            $this->isClass($iterator, 'DOMAIN') ||
222 14
            $this->isType($iterator);
223 14
        $iterator->prev();
224
225 14
        return $isName;
226
    }
227
228 14
    private function isClass(ResourceRecordIterator $iterator, $origin = null): bool
229
    {
230 14
        if (!Classes::isValid($iterator->current())) {
231 14
            return false;
232
        }
233
234 12
        $iterator->next();
235 12
        if ('TTL' === $origin) {
236 10
            $isClass = $this->isType($iterator);
237
        } else {
238 11
            $isClass = $this->isTTL($iterator, 'CLASS') || $this->isType($iterator);
239
        }
240 12
        $iterator->prev();
241
242 12
        return $isClass;
243
    }
244
245 14
    private function isType(ResourceRecordIterator $iterator): bool
246
    {
247 14
        return RDataTypes::isValid(strtoupper($iterator->current())) || array_key_exists($iterator->current(), $this->rdataHandlers);
248
    }
249
250
    /**
251
     * Determine if iterant is a control entry such as $TTL, $ORIGIN, $INCLUDE, etcetera.
252
     *
253
     * @param ResourceRecordIterator $iterator
254
     *
255
     * @return bool
256
     */
257 14
    private function isControlEntry(ResourceRecordIterator $iterator): bool
258
    {
259 14
        return 1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current());
260
    }
261
262
    /**
263
     * Determine if the iterant is a TTL (i.e. it is an integer).
264
     *
265
     * @param ResourceRecordIterator $iterator
266
     * @param string                 $origin
267
     *
268
     * @return bool
269
     */
270 14
    private function isTTL(ResourceRecordIterator $iterator, $origin = null): bool
271
    {
272 14
        if (1 !== preg_match('/^\d+$/', $iterator->current())) {
273 14
            return false;
274
        }
275
276 13
        $iterator->next();
277 13
        if ('CLASS' === $origin) {
278 1
            $isTtl = $this->isType($iterator);
279
        } else {
280 13
            $isTtl = $this->isClass($iterator, 'TTL') || $this->isType($iterator);
281
        }
282 13
        $iterator->prev();
283
284 13
        return $isTtl;
285
    }
286
287
    /**
288
     * @param ResourceRecordIterator $iterator
289
     *
290
     * @return RData\RdataInterface
291
     *
292
     * @throws ParseException
293
     */
294 13
    private function extractRdata(ResourceRecordIterator $iterator): Rdata\RdataInterface
295
    {
296 13
        $type = strtoupper($iterator->current());
297 13
        $iterator->next();
298
299 13
        if (array_key_exists($type, $this->rdataHandlers)) {
300
            try {
301 11
                return call_user_func($this->rdataHandlers[$type], $iterator);
302 2
            } catch (\Exception $exception) {
303 2
                throw new ParseException($exception->getMessage(), null, $exception);
304
            }
305
        }
306
307 8
        return RdataHandlers::catchAll($type, $iterator);
308
    }
309
310
    /**
311
     * This handler addresses the special case where an integer resource name could be confused for a TTL, for instance:
312
     * 50 IN PTR mx1.acme.com.
313
     *
314
     * In the above, if the integer is below 256 then it is assumed to represent an octet of an IPv4 address.
315
     *
316
     * @param ResourceRecordIterator $iterator
317
     *
318
     * @return Rdata\PTR
319
     */
320 3
    private function ptrHandler(ResourceRecordIterator $iterator): Rdata\PTR
321
    {
322 3
        if (null === $this->currentResourceRecord->getName() && null !== $this->currentResourceRecord->getTtl()) {
323 1
            if ($this->currentResourceRecord->getTtl() < 256) {
324 1
                $this->currentResourceRecord->setName((string) $this->currentResourceRecord->getTtl());
325 1
                $this->currentResourceRecord->setTtl(null);
326
            }
327
        }
328
329 3
        $ptr = RdataHandlers::catchAll(Rdata\PTR::TYPE, $iterator);
330 3
        if (!$ptr instanceof Rdata\PTR) {
331
            throw new \UnexpectedValueException();
332
        }
333
334 3
        return $ptr;
335
    }
336
}
337