Passed
Push — master ( 750782...7d3f93 )
by Sam
01:54
created

Parser::getAllRemaining()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
c 0
b 0
f 0
ccs 6
cts 6
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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\Rdata\UnsupportedTypeException;
10
11
class Parser
12
{
13
    /**
14
     * @var string
15
     */
16
    private $string;
17
18
    /**
19
     * @var string
20
     */
21
    private $previousName;
22
23
    /**
24
     * @var Zone
25
     */
26
    private $zone;
27
28
    /**
29
     * @param string $name
30
     * @param string $zone
31
     *
32
     * @return Zone
33
     *
34
     * @throws ParseException
35
     * @throws UnsupportedTypeException
36
     */
37 7
    public static function parse(string $name, string $zone): Zone
38
    {
39 7
        $parser = new self();
40
41 7
        return $parser->makeZone($name, $zone);
42
    }
43
44
    /**
45
     * @param $name
46
     * @param $string
47
     *
48
     * @return Zone
49
     *
50
     * @throws ParseException
51
     * @throws UnsupportedTypeException
52
     */
53 7
    public function makeZone($name, $string): Zone
54
    {
55 7
        $this->zone = new Zone($name);
56 7
        $this->string = Normaliser::normalise($string);
57
58 7
        foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
59 7
            $this->processLine($line);
60
        }
61
62 4
        return $this->zone;
63
    }
64
65
    /**
66
     * @param string $line
67
     *
68
     * @throws UnsupportedTypeException
69
     * @throws ParseException
70
     */
71 7
    private function processLine(string $line)
72
    {
73 7
        $iterator = new \ArrayIterator(explode(Tokens::SPACE, $line));
74
75 7
        if (1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current())) {
76 4
            $this->processControlEntry($iterator);
77
78 4
            return;
79
        }
80
81 7
        $resourceRecord = new ResourceRecord();
82
83
        if (
84 7
            1 === preg_match('/^\d+$/', $iterator->current()) ||
85 7
            Classes::isValid(strtoupper($iterator->current())) ||
86 7
            RDataTypes::isValid(strtoupper($iterator->current()))
87
        ) {
88 1
            $resourceRecord->setName($this->previousName);
89
        } else {
90 7
            $resourceRecord->setName($iterator->current());
91 7
            $this->previousName = $iterator->current();
92 7
            $iterator->next();
93
        }
94
95 7
        $this->processTtl($iterator, $resourceRecord);
96 7
        $this->processClass($iterator, $resourceRecord);
97 7
        $resourceRecord->setRdata($this->extractRdata($iterator));
98
99 4
        $this->zone->addResourceRecord($resourceRecord);
100 4
    }
101
102
    /**
103
     * Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
104
     *
105
     * @param \ArrayIterator $iterator
106
     */
107 4
    private function processControlEntry(\ArrayIterator $iterator): void
108
    {
109 4
        if ('$TTL' === strtoupper($iterator->current())) {
110 4
            $iterator->next();
111 4
            $this->zone->setDefaultTtl((int) $iterator->current());
112
        }
113 4
    }
114
115
    /**
116
     * Set RR's TTL if there is one.
117
     *
118
     * @param \ArrayIterator $iterator
119
     * @param ResourceRecord $resourceRecord
120
     */
121 7
    private function processTtl(\ArrayIterator $iterator, ResourceRecord $resourceRecord)
122
    {
123 7
        if (1 === preg_match('/^\d+$/', $iterator->current())) {
124 4
            $resourceRecord->setTtl($iterator->current());
125 4
            $iterator->next();
126
        }
127 7
    }
128
129
    /**
130
     * Set RR's class if there is one.
131
     *
132
     * @param \ArrayIterator $iterator
133
     * @param ResourceRecord $resourceRecord
134
     */
135 7
    private function processClass(\ArrayIterator $iterator, ResourceRecord $resourceRecord)
136
    {
137 7
        if (Classes::isValid(strtoupper($iterator->current()))) {
138 6
            $resourceRecord->setClass(strtoupper($iterator->current()));
139 6
            $iterator->next();
140
        }
141 7
    }
142
143
    /**
144
     * @param \ArrayIterator $iterator
145
     *
146
     * @return RData\RDataInterface
147
     *
148
     * @throws UnsupportedTypeException
149
     * @throws ParseException
150
     */
151 7
    private function extractRdata(\ArrayIterator $iterator): Rdata\RdataInterface
152
    {
153 7
        $type = strtoupper($iterator->current());
154 7
        $iterator->next();
155
156 7
        if (!Rdata\Factory::isTypeImplemented($type)) {
157 1
            throw new UnsupportedTypeException($type);
158
        }
159
160
        switch ($type) {
161 6
            case Rdata\LOC::TYPE:
162 1
                return $this->handleLocRdata($iterator);
163 6
            case Rdata\TXT::TYPE:
164 3
                return $this->handleTxtRdata($iterator);
165 6
            case Rdata\APL::TYPE:
166 4
                return $this->handleAplRdata($iterator);
167
        }
168
169 4
        return call_user_func_array(['\\Badcow\\DNS\\Rdata\\Factory', $type], $this->getAllRemaining($iterator));
170
    }
171
172
    /**
173
     * @param \ArrayIterator $iterator
174
     *
175
     * @return Rdata\TXT
176
     *
177
     * @throws ParseException
178
     */
179 3
    private function handleTxtRdata(\ArrayIterator $iterator): Rdata\TXT
180
    {
181 3
        $string = new StringIterator(implode(Tokens::SPACE, self::getAllRemaining($iterator)));
0 ignored issues
show
Bug Best Practice introduced by
The method Badcow\DNS\Parser\Parser::getAllRemaining() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

181
        $string = new StringIterator(implode(Tokens::SPACE, self::/** @scrutinizer ignore-call */ getAllRemaining($iterator)));
Loading history...
182 3
        $txt = new StringIterator();
183 3
        $doubleQuotesOpen = false;
184
185 3
        while ($string->valid()) {
186 3
            switch ($string->current()) {
187 3
                case Tokens::BACKSLASH:
188 3
                    $string->next();
189 3
                    $txt->append($string->current());
190 3
                    $string->next();
191 3
                    break;
192 3
                case Tokens::DOUBLE_QUOTES:
193 3
                    $doubleQuotesOpen = !$doubleQuotesOpen;
0 ignored issues
show
introduced by
The condition $doubleQuotesOpen is always false.
Loading history...
194 3
                    $string->next();
195 3
                    break;
196
                default:
197 3
                    if ($doubleQuotesOpen) {
198 3
                        $txt->append($string->current());
199
                    }
200 3
                    $string->next();
201 3
                    break;
202
            }
203
        }
204
205 3
        if ($doubleQuotesOpen) {
206
            throw new ParseException('Unbalanced double quotation marks.');
207
        }
208
209 3
        return Rdata\Factory::txt((string) $txt);
210
    }
211
212
    /**
213
     * Return current entry and moves the iterator to the next entry.
214
     *
215
     * @param \ArrayIterator $iterator
216
     *
217
     * @return mixed
218
     */
219 1
    private function pop(\ArrayIterator $iterator)
220
    {
221 1
        $current = $iterator->current();
222 1
        $iterator->next();
223
224 1
        return $current;
225
    }
226
227
    /**
228
     * Get all the remaining values of an iterator as an array.
229
     *
230
     * @param \ArrayIterator $iterator
231
     *
232
     * @return array
233
     */
234 4
    private function getAllRemaining(\ArrayIterator $iterator): array
235
    {
236 4
        $values = [];
237 4
        while ($iterator->valid()) {
238 4
            $values[] = $iterator->current();
239 4
            $iterator->next();
240
        }
241
242 4
        return $values;
243
    }
244
245
    /**
246
     * Transform a DMS string to a decimal representation. Used for LOC records.
247
     *
248
     * @param int    $deg        Degrees
249
     * @param int    $min        Minutes
250
     * @param float  $sec        Seconds
251
     * @param string $hemisphere Either 'N', 'S', 'E', or 'W'
252
     *
253
     * @return float
254
     */
255 1
    private function dmsToDecimal(int $deg, int $min, float $sec, string $hemisphere): float
256
    {
257 1
        $multiplier = ('S' === $hemisphere || 'W' === $hemisphere) ? -1 : 1;
258
259 1
        return $multiplier * ($deg + ($min / 60) + ($sec / 3600));
260
    }
261
262
    /**
263
     * @param \ArrayIterator $iterator
264
     *
265
     * @return Rdata\LOC
266
     */
267 1
    private function handleLocRdata(\ArrayIterator $iterator): Rdata\LOC
268
    {
269 1
        $lat = $this->dmsToDecimal($this->pop($iterator), $this->pop($iterator), $this->pop($iterator), $this->pop($iterator));
270 1
        $lon = $this->dmsToDecimal($this->pop($iterator), $this->pop($iterator), $this->pop($iterator), $this->pop($iterator));
271
272 1
        return Rdata\Factory::Loc(
273 1
            $lat,
274 1
            $lon,
275 1
            (float) $this->pop($iterator),
276 1
            (float) $this->pop($iterator),
277 1
            (float) $this->pop($iterator),
278 1
            (float) $this->pop($iterator)
279
        );
280
    }
281
282
    /**
283
     * @param \ArrayIterator $iterator
284
     *
285
     * @return Rdata\APL
286
     *
287
     * @throws ParseException
288
     */
289 4
    private function handleAplRdata(\ArrayIterator $iterator): Rdata\APL
290
    {
291 4
        $rdata = new Rdata\APL();
292
293 4
        while ($iterator->valid()) {
294 4
            $matches = [];
295 4
            if (1 !== preg_match('/^(?<negate>!)?[1-2]:(?<block>.+)$/i', $iterator->current(), $matches)) {
296 2
                throw new ParseException(sprintf('"%s" is not a valid IP range.', $iterator->current()));
297
            }
298
299 2
            $ipBlock = \IPBlock::create($matches['block']);
300 2
            $rdata->addAddressRange($ipBlock, '!' !== $matches['negate']);
301 2
            $iterator->next();
302
        }
303
304 2
        return $rdata;
305
    }
306
}
307