Completed
Push — master ( 6436dc...73f8f9 )
by Sam
22s queued 11s
created

Parser::isClass()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
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 12
    public function __construct(array $rdataHandlers = [])
64
    {
65 12
        $this->rdataHandlers = array_merge(RdataHandlers::getHandlers(), $rdataHandlers);
66 12
    }
67
68
    /**
69
     * @param string $name
70
     * @param string $zone
71
     *
72
     * @return Zone
73
     *
74
     * @throws ParseException
75
     */
76 11
    public static function parse(string $name, string $zone): Zone
77
    {
78 11
        return (new self())->makeZone($name, $zone);
79
    }
80
81
    /**
82
     * @param string $name
83
     * @param string $string
84
     *
85
     * @return Zone
86
     *
87
     * @throws ParseException
88
     */
89 12
    public function makeZone(string $name, string $string): Zone
90
    {
91 12
        $this->zone = new Zone($name);
92 12
        $this->string = Normaliser::normalise($string);
93
94 12
        foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
95 12
            $this->processLine($line);
96
        }
97
98 9
        return $this->zone;
99
    }
100
101
    /**
102
     * @param string $line
103
     *
104
     * @throws ParseException
105
     */
106 12
    private function processLine(string $line): void
107
    {
108 12
        $iterator = new ResourceRecordIterator($line);
109
110 12
        if ($this->isControlEntry($iterator)) {
111 6
            $this->processControlEntry($iterator);
112
113 6
            return;
114
        }
115
116 12
        $this->currentResourceRecord = new ResourceRecord();
117 12
        $this->processEntry($iterator);
118 9
        $this->zone->addResourceRecord($this->currentResourceRecord);
119 9
    }
120
121
    /**
122
     * @param ResourceRecordIterator $iterator
123
     *
124
     * @throws ParseException
125
     */
126 12
    private function processEntry(ResourceRecordIterator $iterator): void
127
    {
128 12
        if ($this->isTTL($iterator)) {
129 7
            $this->currentResourceRecord->setTtl((int) $iterator->current());
130 7
            $this->lastStatedTtl = (int) $iterator->current();
131 7
            $iterator->next();
132 7
            $this->processEntry($iterator);
133
134 5
            return;
135
        }
136
137 12
        if ($this->isClass($iterator)) {
138 9
            $this->currentResourceRecord->setClass(strtoupper($iterator->current()));
139 9
            $this->lastStatedClass = strtoupper($iterator->current());
140 9
            $iterator->next();
141 9
            $this->processEntry($iterator);
142
143 7
            return;
144
        }
145
146 12
        if ($this->isResourceName($iterator) && null === $this->currentResourceRecord->getName()) {
147 11
            $this->currentResourceRecord->setName($iterator->current());
148 11
            $this->lastStatedDomain = $iterator->current();
149 11
            $iterator->next();
150 11
            $this->processEntry($iterator);
151
152 9
            return;
153
        }
154
155 12
        if ($this->isType($iterator)) {
156 11
            $this->currentResourceRecord->setRdata($this->extractRdata($iterator));
157 9
            $this->populateWithLastStated();
158
159 9
            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 9
    private function populateWithLastStated(): void
171
    {
172 9
        if (null === $this->currentResourceRecord->getName()) {
173 2
            $this->currentResourceRecord->setName($this->lastStatedDomain);
174
        }
175
176 9
        if (null === $this->currentResourceRecord->getTtl()) {
177 6
            $this->currentResourceRecord->setTtl($this->lastStatedTtl);
178
        }
179
180 9
        if (null === $this->currentResourceRecord->getClass()) {
181 6
            $this->currentResourceRecord->setClass($this->lastStatedClass);
182
        }
183 9
    }
184
185
    /**
186
     * Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
187
     *
188
     * @param ResourceRecordIterator $iterator
189
     */
190 6
    private function processControlEntry(ResourceRecordIterator $iterator): void
191
    {
192 6
        if ('$TTL' === strtoupper($iterator->current())) {
193 6
            $iterator->next();
194 6
            $this->zone->setDefaultTtl((int) $iterator->current());
195
        }
196 6
    }
197
198
    /**
199
     * Determine if iterant is a resource name.
200
     *
201
     * @param ResourceRecordIterator $iterator
202
     *
203
     * @return bool
204
     */
205 12
    private function isResourceName(ResourceRecordIterator $iterator): bool
206
    {
207 12
        $iterator->next();
208
209 12
        if (!$iterator->valid()) {
210
            return false;
211
        }
212
213 12
        $isName = $this->isTTL($iterator) ||
214 12
            $this->isClass($iterator) ||
215 12
            $this->isType($iterator);
216 12
        $iterator->prev();
217
218 12
        return $isName;
219
    }
220
221 12
    private function isClass(ResourceRecordIterator $iterator): bool
222
    {
223 12
        if (!Classes::isValid($iterator->current())) {
224 12
            return false;
225
        }
226
227 10
        $iterator->next();
228 10
        $isClass = $this->isTTL($iterator) || $this->isType($iterator);
229 10
        $iterator->prev();
230
231 10
        return $isClass;
232
    }
233
234 12
    private function isType(ResourceRecordIterator $iterator): bool
235
    {
236 12
        return RDataTypes::isValid(strtoupper($iterator->current())) || array_key_exists($iterator->current(), $this->rdataHandlers);
237
    }
238
239
    /**
240
     * Determine if iterant is a control entry such as $TTL, $ORIGIN, $INCLUDE, etcetera.
241
     *
242
     * @param ResourceRecordIterator $iterator
243
     *
244
     * @return bool
245
     */
246 12
    private function isControlEntry(ResourceRecordIterator $iterator): bool
247
    {
248 12
        return 1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current());
249
    }
250
251
    /**
252
     * Determine if the iterant is a TTL (i.e. it is an integer).
253
     *
254
     * @param ResourceRecordIterator $iterator
255
     *
256
     * @return bool
257
     */
258 12
    private function isTTL(ResourceRecordIterator $iterator): bool
259
    {
260 12
        if (1 !== preg_match('/^\d+$/', $iterator->current())) {
261 12
            return false;
262
        }
263
264 11
        $iterator->next();
265 11
        $isTtl = $this->isClass($iterator) || $this->isType($iterator);
266 11
        $iterator->prev();
267
268 11
        return $isTtl;
269
    }
270
271
    /**
272
     * @param ResourceRecordIterator $iterator
273
     *
274
     * @return RData\RdataInterface
275
     *
276
     * @throws ParseException
277
     */
278 11
    private function extractRdata(ResourceRecordIterator $iterator): Rdata\RdataInterface
279
    {
280 11
        $type = strtoupper($iterator->current());
281 11
        $iterator->next();
282
283 11
        if (array_key_exists($type, $this->rdataHandlers)) {
284
            try {
285 9
                return call_user_func($this->rdataHandlers[$type], $iterator);
286 2
            } catch (\Exception $exception) {
287 2
                throw new ParseException($exception->getMessage(), null, $exception);
288
            }
289
        }
290
291 7
        return RdataHandlers::catchAll($type, $iterator);
292
    }
293
}
294