Passed
Branch master (750782)
by Sam
02:20
created

Parser   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Test Coverage

Coverage 99.07%

Importance

Changes 0
Metric Value
wmc 35
eloc 102
dl 0
loc 288
ccs 106
cts 107
cp 0.9907
rs 9.6
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A dmsToDecimal() 0 5 3
A pop() 0 6 1
A parse() 0 5 1
A processClass() 0 5 2
A processTtl() 0 5 2
A processControlEntry() 0 5 2
A processLine() 0 29 5
A makeZone() 0 10 2
A handleLocRdata() 0 12 1
A handleAplRdata() 0 16 3
A extractRdata() 0 25 6
B handleTxtRdata() 0 37 7
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(' ', $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 6
        switch ($type) {
161
            case Rdata\LOC::TYPE:
162 1
                return $this->handleLocRdata($iterator);
163
            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
        $parameters = [];
170
171 4
        while ($iterator->valid()) {
172 4
            $parameters[] = $this->pop($iterator);
173
        }
174
175 4
        return call_user_func_array(['\\Badcow\\DNS\\Rdata\\Factory', $type], $parameters);
176
    }
177
178
    /**
179
     * @param \ArrayIterator $iterator
180
     *
181
     * @return Rdata\TXT
182
     *
183
     * @throws ParseException
184
     */
185 3
    private function handleTxtRdata(\ArrayIterator $iterator): Rdata\TXT
186
    {
187 3
        $txt = '';
188 3
        while ($iterator->valid()) {
189 3
            $txt .= $this->pop($iterator).' ';
190
        }
191 3
        $txt = substr($txt, 0, -1);
192
193 3
        $string = new StringIterator($txt);
194 3
        $txt = '';
195 3
        $doubleQuotesOpen = false;
196
197 3
        while ($string->valid()) {
198 3
            switch ($string->current()) {
199
                case Tokens::BACKSLASH:
200 3
                    $string->next();
201 3
                    $txt .= $string->current();
202 3
                    $string->next();
203 3
                    break;
204
                case Tokens::DOUBLE_QUOTES:
205 3
                    $doubleQuotesOpen = !$doubleQuotesOpen;
0 ignored issues
show
introduced by
The condition $doubleQuotesOpen is always false.
Loading history...
206 3
                    $string->next();
207 3
                    break;
208
                default:
209 3
                    if ($doubleQuotesOpen) {
210 3
                        $txt .= $string->current();
211
                    }
212 3
                    $string->next();
213 3
                    break;
214
            }
215
        }
216
217 3
        if ($doubleQuotesOpen) {
218
            throw new ParseException('Unbalanced double quotation marks.');
219
        }
220
221 3
        return Rdata\Factory::txt($txt);
222
    }
223
224
    /**
225
     * Return current entry and moves the iterator to the next entry.
226
     *
227
     * @param \ArrayIterator $arrayIterator
228
     *
229
     * @return mixed
230
     */
231 4
    private function pop(\ArrayIterator $arrayIterator)
232
    {
233 4
        $current = $arrayIterator->current();
234 4
        $arrayIterator->next();
235
236 4
        return $current;
237
    }
238
239
    /**
240
     * Transform a DMS string to a decimal representation. Used for LOC records.
241
     *
242
     * @param int    $deg
243
     * @param int    $m
244
     * @param float  $s
245
     * @param string $hemisphere
246
     *
247
     * @return float
248
     */
249 1
    private function dmsToDecimal(int $deg, int $m, float $s, string $hemisphere): float
250
    {
251 1
        $multiplier = ('S' === $hemisphere || 'W' === $hemisphere) ? -1 : 1;
252
253 1
        return $multiplier * ($deg + ($m / 60) + ($s / 3600));
254
    }
255
256
    /**
257
     * @param \ArrayIterator $iterator
258
     *
259
     * @return Rdata\LOC
260
     */
261 1
    private function handleLocRdata(\ArrayIterator $iterator): Rdata\LOC
262
    {
263 1
        $lat = $this->dmsToDecimal($this->pop($iterator), $this->pop($iterator), $this->pop($iterator), $this->pop($iterator));
264 1
        $lon = $this->dmsToDecimal($this->pop($iterator), $this->pop($iterator), $this->pop($iterator), $this->pop($iterator));
265
266 1
        return Rdata\Factory::Loc(
267 1
            $lat,
268 1
            $lon,
269 1
            (float) $this->pop($iterator),
270 1
            (float) $this->pop($iterator),
271 1
            (float) $this->pop($iterator),
272 1
            (float) $this->pop($iterator)
273
        );
274
    }
275
276
    /**
277
     * @param \ArrayIterator $iterator
278
     *
279
     * @return Rdata\APL
280
     *
281
     * @throws ParseException
282
     */
283 4
    private function handleAplRdata(\ArrayIterator $iterator): Rdata\APL
284
    {
285 4
        $rdata = new Rdata\APL();
286
287 4
        while ($iterator->valid()) {
288 4
            $matches = [];
289 4
            if (1 !== preg_match('/^(?<negate>!)?[1-2]:(?<block>.+)$/i', $iterator->current(), $matches)) {
290 2
                throw new ParseException(sprintf('"%s" is not a valid IP range.', $iterator->current()));
291
            }
292
293 2
            $ipBlock = \IPBlock::create($matches['block']);
294 2
            $rdata->addAddressRange($ipBlock, '!' !== $matches['negate']);
295 2
            $iterator->next();
296
        }
297
298 2
        return $rdata;
299
    }
300
}
301