Completed
Push — master ( 49512c...b4a71b )
by Sam
01:55
created

Parser::handleAplRdata()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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