Passed
Push — master ( 85041f...57eb77 )
by Sam
01:45
created

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