Passed
Push — master ( 742734...0d072d )
by Sam
01:53
created

Parser::isControlEntry()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Badcow\DNS\Parser;
4
5
use Badcow\DNS\Classes;
6
use Badcow\DNS\ResourceRecord;
7
use Badcow\DNS\Zone;
8
use Badcow\DNS\Rdata;
9
10
class Parser
11
{
12
    /**
13
     * @var string
14
     */
15
    private $string;
16
17
    /**
18
     * @var string
19
     */
20
    private $previousName;
21
22
    /**
23
     * @var Zone
24
     */
25
    private $zone;
26
27
    /**
28
     * @param string $name
29
     * @param string $zone
30
     *
31
     * @return Zone
32
     *
33
     * @throws ParseException
34
     */
35 7
    public static function parse(string $name, string $zone): Zone
36
    {
37 7
        $parser = new self();
38
39 7
        return $parser->makeZone($name, $zone);
40
    }
41
42
    /**
43
     * @param $name
44
     * @param $string
45
     *
46
     * @return Zone
47
     *
48
     * @throws ParseException
49
     */
50 7
    public function makeZone($name, $string): Zone
51
    {
52 7
        $this->zone = new Zone($name);
53 7
        $this->string = Normaliser::normalise($string);
54
55 7
        foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
56 7
            $this->processLine($line);
57
        }
58
59 5
        return $this->zone;
60
    }
61
62
    /**
63
     * @param string $line
64
     *
65
     * @throws ParseException
66
     */
67 7
    private function processLine(string $line)
68
    {
69 7
        $iterator = new \ArrayIterator(explode(Tokens::SPACE, $line));
70
71 7
        if ($this->isControlEntry($iterator)) {
72 4
            $this->processControlEntry($iterator);
73
74 4
            return;
75
        }
76
77 7
        $resourceRecord = new ResourceRecord();
78
79 7
        $this->processResourceName($iterator, $resourceRecord);
80 7
        $this->processTtl($iterator, $resourceRecord);
81 7
        $this->processClass($iterator, $resourceRecord);
82 7
        $resourceRecord->setRdata($this->extractRdata($iterator));
83
84 5
        $this->zone->addResourceRecord($resourceRecord);
85 5
    }
86
87
    /**
88
     * Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
89
     *
90
     * @param \ArrayIterator $iterator
91
     */
92 4
    private function processControlEntry(\ArrayIterator $iterator): void
93
    {
94 4
        if ('$TTL' === strtoupper($iterator->current())) {
95 4
            $iterator->next();
96 4
            $this->zone->setDefaultTtl((int) $iterator->current());
97
        }
98 4
    }
99
100
    /**
101
     * Processes a ResourceRecord name.
102
     *
103
     * @param \ArrayIterator $iterator
104
     * @param ResourceRecord $resourceRecord
105
     */
106 7
    private function processResourceName(\ArrayIterator $iterator, ResourceRecord $resourceRecord): void
107
    {
108 7
        if ($this->isResourceName($iterator)) {
109 7
            $this->previousName = $iterator->current();
110 7
            $iterator->next();
111
        }
112
113 7
        $resourceRecord->setName($this->previousName);
114 7
    }
115
116
    /**
117
     * Set RR's TTL if there is one.
118
     *
119
     * @param \ArrayIterator $iterator
120
     * @param ResourceRecord $resourceRecord
121
     */
122 7
    private function processTtl(\ArrayIterator $iterator, ResourceRecord $resourceRecord): void
123
    {
124 7
        if (1 === preg_match('/^\d+$/', $iterator->current())) {
125 4
            $resourceRecord->setTtl($iterator->current());
126 4
            $iterator->next();
127
        }
128 7
    }
129
130
    /**
131
     * Set RR's class if there is one.
132
     *
133
     * @param \ArrayIterator $iterator
134
     * @param ResourceRecord $resourceRecord
135
     */
136 7
    private function processClass(\ArrayIterator $iterator, ResourceRecord $resourceRecord): void
137
    {
138 7
        if (Classes::isValid(strtoupper($iterator->current()))) {
139 6
            $resourceRecord->setClass(strtoupper($iterator->current()));
140 6
            $iterator->next();
141
        }
142 7
    }
143
144
    /**
145
     * Determine if iterant is a resource name.
146
     *
147
     * @param \ArrayIterator $iterator
148
     *
149
     * @return bool
150
     */
151 7
    private function isResourceName(\ArrayIterator $iterator): bool
152
    {
153
        return !(
154 7
            preg_match('/^\d+$/', $iterator->current()) ||
155 7
            Classes::isValid(strtoupper($iterator->current())) ||
156 7
            RDataTypes::isValid(strtoupper($iterator->current()))
157
        );
158
    }
159
160
    /**
161
     * Determine if iterant is a control entry such as $TTL, $ORIGIN, $INCLUDE, etcetera.
162
     *
163
     * @param \ArrayIterator $iterator
164
     *
165
     * @return bool
166
     */
167 7
    private function isControlEntry(\ArrayIterator $iterator): bool
168
    {
169 7
        return 1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current());
170
    }
171
172
    /**
173
     * @param \ArrayIterator $iterator
174
     *
175
     * @return RData\RDataInterface
176
     *
177
     * @throws ParseException
178
     */
179 7
    private function extractRdata(\ArrayIterator $iterator): Rdata\RdataInterface
180
    {
181 7
        $type = strtoupper($iterator->current());
182 7
        $iterator->next();
183
184 7
        if (!Rdata\Factory::isTypeImplemented($type)) {
185 1
            return new PolymorphicRdata($type, implode(' ', $this->getAllRemaining($iterator)));
186
        }
187
188
        switch ($type) {
189 6
            case Rdata\LOC::TYPE:
190 1
                return $this->handleLocRdata($iterator);
191 6
            case Rdata\TXT::TYPE:
192 3
                return $this->handleTxtRdata($iterator);
193 6
            case Rdata\APL::TYPE:
194 4
                return $this->handleAplRdata($iterator);
195
        }
196
197 4
        return call_user_func_array(['\\Badcow\\DNS\\Rdata\\Factory', $type], $this->getAllRemaining($iterator));
198
    }
199
200
    /**
201
     * @param \ArrayIterator $iterator
202
     *
203
     * @return Rdata\TXT
204
     *
205
     * @throws ParseException
206
     */
207 3
    private function handleTxtRdata(\ArrayIterator $iterator): Rdata\TXT
208
    {
209 3
        $string = new StringIterator(implode(Tokens::SPACE, $this->getAllRemaining($iterator)));
210 3
        $txt = new StringIterator();
211 3
        $doubleQuotesOpen = false;
212
213 3
        while ($string->valid()) {
214 3
            switch ($string->current()) {
215 3
                case Tokens::BACKSLASH:
216 3
                    $string->next();
217 3
                    $txt->append($string->current());
218 3
                    $string->next();
219 3
                    break;
220 3
                case Tokens::DOUBLE_QUOTES:
221 3
                    $doubleQuotesOpen = !$doubleQuotesOpen;
0 ignored issues
show
introduced by
The condition $doubleQuotesOpen is always false.
Loading history...
222 3
                    $string->next();
223 3
                    break;
224
                default:
225 3
                    if ($doubleQuotesOpen) {
226 3
                        $txt->append($string->current());
227
                    }
228 3
                    $string->next();
229 3
                    break;
230
            }
231
        }
232
233 3
        if ($doubleQuotesOpen) {
234
            throw new ParseException('Unbalanced double quotation marks.');
235
        }
236
237 3
        return Rdata\Factory::txt((string) $txt);
238
    }
239
240
    /**
241
     * Return current entry and moves the iterator to the next entry.
242
     *
243
     * @param \ArrayIterator $iterator
244
     *
245
     * @return mixed
246
     */
247 1
    private function pop(\ArrayIterator $iterator)
248
    {
249 1
        $current = $iterator->current();
250 1
        $iterator->next();
251
252 1
        return $current;
253
    }
254
255
    /**
256
     * Get all the remaining values of an iterator as an array.
257
     *
258
     * @param \ArrayIterator $iterator
259
     *
260
     * @return array
261
     */
262 5
    private function getAllRemaining(\ArrayIterator $iterator): array
263
    {
264 5
        $values = [];
265 5
        while ($iterator->valid()) {
266 5
            $values[] = $iterator->current();
267 5
            $iterator->next();
268
        }
269
270 5
        return $values;
271
    }
272
273
    /**
274
     * Transform a DMS string to a decimal representation. Used for LOC records.
275
     *
276
     * @param int    $deg        Degrees
277
     * @param int    $min        Minutes
278
     * @param float  $sec        Seconds
279
     * @param string $hemisphere Either 'N', 'S', 'E', or 'W'
280
     *
281
     * @return float
282
     */
283 1
    private function dmsToDecimal(int $deg, int $min, float $sec, string $hemisphere): float
284
    {
285 1
        $multiplier = ('S' === $hemisphere || 'W' === $hemisphere) ? -1 : 1;
286
287 1
        return $multiplier * ($deg + ($min / 60) + ($sec / 3600));
288
    }
289
290
    /**
291
     * @param \ArrayIterator $iterator
292
     *
293
     * @return Rdata\LOC
294
     */
295 1
    private function handleLocRdata(\ArrayIterator $iterator): Rdata\LOC
296
    {
297 1
        $lat = $this->dmsToDecimal($this->pop($iterator), $this->pop($iterator), $this->pop($iterator), $this->pop($iterator));
298 1
        $lon = $this->dmsToDecimal($this->pop($iterator), $this->pop($iterator), $this->pop($iterator), $this->pop($iterator));
299
300 1
        return Rdata\Factory::Loc(
301 1
            $lat,
302 1
            $lon,
303 1
            (float) $this->pop($iterator),
304 1
            (float) $this->pop($iterator),
305 1
            (float) $this->pop($iterator),
306 1
            (float) $this->pop($iterator)
307
        );
308
    }
309
310
    /**
311
     * @param \ArrayIterator $iterator
312
     *
313
     * @return Rdata\APL
314
     *
315
     * @throws ParseException
316
     */
317 4
    private function handleAplRdata(\ArrayIterator $iterator): Rdata\APL
318
    {
319 4
        $rdata = new Rdata\APL();
320
321 4
        while ($iterator->valid()) {
322 4
            $matches = [];
323 4
            if (1 !== preg_match('/^(?<negate>!)?[1-2]:(?<block>.+)$/i', $iterator->current(), $matches)) {
324 2
                throw new ParseException(sprintf('"%s" is not a valid IP range.', $iterator->current()));
325
            }
326
327 2
            $ipBlock = \IPBlock::create($matches['block']);
328 2
            $rdata->addAddressRange($ipBlock, '!' !== $matches['negate']);
329 2
            $iterator->next();
330
        }
331
332 2
        return $rdata;
333
    }
334
}
335