Passed
Push — master ( 66e2c6...742734 )
by Sam
03:55
created

Parser::handleTxtRdata()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.0045

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 31
ccs 19
cts 20
cp 0.95
rs 8.9457
c 0
b 0
f 0
cc 6
nc 10
nop 1
crap 6.0045
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 (1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current())) {
72 4
            $this->processControlEntry($iterator);
73
74 4
            return;
75
        }
76
77 7
        $resourceRecord = new ResourceRecord();
78
79
        if (
80 7
            1 === preg_match('/^\d+$/', $iterator->current()) ||
81 7
            Classes::isValid(strtoupper($iterator->current())) ||
82 7
            RDataTypes::isValid(strtoupper($iterator->current()))
83
        ) {
84 1
            $resourceRecord->setName($this->previousName);
85
        } else {
86 7
            $resourceRecord->setName($iterator->current());
87 7
            $this->previousName = $iterator->current();
88 7
            $iterator->next();
89
        }
90
91 7
        $this->processTtl($iterator, $resourceRecord);
92 7
        $this->processClass($iterator, $resourceRecord);
93 7
        $resourceRecord->setRdata($this->extractRdata($iterator));
94
95 5
        $this->zone->addResourceRecord($resourceRecord);
96 5
    }
97
98
    /**
99
     * Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
100
     *
101
     * @param \ArrayIterator $iterator
102
     */
103 4
    private function processControlEntry(\ArrayIterator $iterator): void
104
    {
105 4
        if ('$TTL' === strtoupper($iterator->current())) {
106 4
            $iterator->next();
107 4
            $this->zone->setDefaultTtl((int) $iterator->current());
108
        }
109 4
    }
110
111
    /**
112
     * Set RR's TTL if there is one.
113
     *
114
     * @param \ArrayIterator $iterator
115
     * @param ResourceRecord $resourceRecord
116
     */
117 7
    private function processTtl(\ArrayIterator $iterator, ResourceRecord $resourceRecord)
118
    {
119 7
        if (1 === preg_match('/^\d+$/', $iterator->current())) {
120 4
            $resourceRecord->setTtl($iterator->current());
121 4
            $iterator->next();
122
        }
123 7
    }
124
125
    /**
126
     * Set RR's class if there is one.
127
     *
128
     * @param \ArrayIterator $iterator
129
     * @param ResourceRecord $resourceRecord
130
     */
131 7
    private function processClass(\ArrayIterator $iterator, ResourceRecord $resourceRecord)
132
    {
133 7
        if (Classes::isValid(strtoupper($iterator->current()))) {
134 6
            $resourceRecord->setClass(strtoupper($iterator->current()));
135 6
            $iterator->next();
136
        }
137 7
    }
138
139
    /**
140
     * @param \ArrayIterator $iterator
141
     *
142
     * @return RData\RDataInterface
143
     *
144
     * @throws ParseException
145
     */
146 7
    private function extractRdata(\ArrayIterator $iterator): Rdata\RdataInterface
147
    {
148 7
        $type = strtoupper($iterator->current());
149 7
        $iterator->next();
150
151 7
        if (!Rdata\Factory::isTypeImplemented($type)) {
152 1
            return new PolymorphicRdata($type, implode(' ', $this->getAllRemaining($iterator)));
153
        }
154
155 6
        switch ($type) {
156
            case Rdata\LOC::TYPE:
157 1
                return $this->handleLocRdata($iterator);
158
            case Rdata\TXT::TYPE:
159 3
                return $this->handleTxtRdata($iterator);
160 6
            case Rdata\APL::TYPE:
161 4
                return $this->handleAplRdata($iterator);
162
        }
163
164 4
        return call_user_func_array(['\\Badcow\\DNS\\Rdata\\Factory', $type], $this->getAllRemaining($iterator));
165
    }
166
167
    /**
168
     * @param \ArrayIterator $iterator
169
     *
170
     * @return Rdata\TXT
171
     *
172
     * @throws ParseException
173
     */
174 3
    private function handleTxtRdata(\ArrayIterator $iterator): Rdata\TXT
175
    {
176 3
        $string = new StringIterator(implode(Tokens::SPACE, $this->getAllRemaining($iterator)));
177 3
        $txt = new StringIterator();
178 3
        $doubleQuotesOpen = false;
179
180 3
        while ($string->valid()) {
181 3
            switch ($string->current()) {
182
                case Tokens::BACKSLASH:
183 3
                    $string->next();
184 3
                    $txt->append($string->current());
185 3
                    $string->next();
186 3
                    break;
187
                case Tokens::DOUBLE_QUOTES:
188 3
                    $doubleQuotesOpen = !$doubleQuotesOpen;
0 ignored issues
show
introduced by
The condition $doubleQuotesOpen is always false.
Loading history...
189 3
                    $string->next();
190 3
                    break;
191
                default:
192 3
                    if ($doubleQuotesOpen) {
193 3
                        $txt->append($string->current());
194
                    }
195 3
                    $string->next();
196 3
                    break;
197
            }
198
        }
199
200 3
        if ($doubleQuotesOpen) {
201
            throw new ParseException('Unbalanced double quotation marks.');
202
        }
203
204 3
        return Rdata\Factory::txt((string) $txt);
205
    }
206
207
    /**
208
     * Return current entry and moves the iterator to the next entry.
209
     *
210
     * @param \ArrayIterator $iterator
211
     *
212
     * @return mixed
213
     */
214 1
    private function pop(\ArrayIterator $iterator)
215
    {
216 1
        $current = $iterator->current();
217 1
        $iterator->next();
218
219 1
        return $current;
220
    }
221
222
    /**
223
     * Get all the remaining values of an iterator as an array.
224
     *
225
     * @param \ArrayIterator $iterator
226
     *
227
     * @return array
228
     */
229 5
    private function getAllRemaining(\ArrayIterator $iterator): array
230
    {
231 5
        $values = [];
232 5
        while ($iterator->valid()) {
233 5
            $values[] = $iterator->current();
234 5
            $iterator->next();
235
        }
236
237 5
        return $values;
238
    }
239
240
    /**
241
     * Transform a DMS string to a decimal representation. Used for LOC records.
242
     *
243
     * @param int    $deg        Degrees
244
     * @param int    $min        Minutes
245
     * @param float  $sec        Seconds
246
     * @param string $hemisphere Either 'N', 'S', 'E', or 'W'
247
     *
248
     * @return float
249
     */
250 1
    private function dmsToDecimal(int $deg, int $min, float $sec, string $hemisphere): float
251
    {
252 1
        $multiplier = ('S' === $hemisphere || 'W' === $hemisphere) ? -1 : 1;
253
254 1
        return $multiplier * ($deg + ($min / 60) + ($sec / 3600));
255
    }
256
257
    /**
258
     * @param \ArrayIterator $iterator
259
     *
260
     * @return Rdata\LOC
261
     */
262 1
    private function handleLocRdata(\ArrayIterator $iterator): Rdata\LOC
263
    {
264 1
        $lat = $this->dmsToDecimal($this->pop($iterator), $this->pop($iterator), $this->pop($iterator), $this->pop($iterator));
265 1
        $lon = $this->dmsToDecimal($this->pop($iterator), $this->pop($iterator), $this->pop($iterator), $this->pop($iterator));
266
267 1
        return Rdata\Factory::Loc(
268 1
            $lat,
269 1
            $lon,
270 1
            (float) $this->pop($iterator),
271 1
            (float) $this->pop($iterator),
272 1
            (float) $this->pop($iterator),
273 1
            (float) $this->pop($iterator)
274
        );
275
    }
276
277
    /**
278
     * @param \ArrayIterator $iterator
279
     *
280
     * @return Rdata\APL
281
     *
282
     * @throws ParseException
283
     */
284 4
    private function handleAplRdata(\ArrayIterator $iterator): Rdata\APL
285
    {
286 4
        $rdata = new Rdata\APL();
287
288 4
        while ($iterator->valid()) {
289 4
            $matches = [];
290 4
            if (1 !== preg_match('/^(?<negate>!)?[1-2]:(?<block>.+)$/i', $iterator->current(), $matches)) {
291 2
                throw new ParseException(sprintf('"%s" is not a valid IP range.', $iterator->current()));
292
            }
293
294 2
            $ipBlock = \IPBlock::create($matches['block']);
295 2
            $rdata->addAddressRange($ipBlock, '!' !== $matches['negate']);
296 2
            $iterator->next();
297
        }
298
299 2
        return $rdata;
300
    }
301
}
302