Completed
Push — master ( 577829...a29856 )
by Aurimas
04:44
created

Decoder::decode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 2
nop 1
crap 2
1
<?php
2
3
namespace Akatsuki\Component\Bencode;
4
5
use Akatsuki\Component\Bencode\Exception\InvalidSourceException;
6
7
/**
8
 * Class Decoder
9
 *
10
 * @package Akatsuki\Component\Bencode
11
 * @author  Aurimas Niekis <[email protected]>
12
 */
13
class Decoder
14
{
15
    /**
16
     * @var string
17
     */
18
    private $source;
19
20
    /**
21
     * @var int
22
     */
23
    private $sourceLength;
24
25
    /**
26
     * @var int
27
     */
28
    private $offset;
29
30 17
    public function decode(string $source)
31
    {
32 17
        $this->source       = $source;
33 17
        $this->sourceLength = strlen($this->source);
34 17
        $this->offset       = 0;
35
36 17
        $decoded = $this->doDecode();
37
38 5
        if ($this->offset !== $this->sourceLength) {
39 1
            throw new InvalidSourceException('Found multiple entities outside list or dict definitions');
40
        }
41
42 4
        return $decoded;
43
    }
44
45 17
    private function doDecode()
46
    {
47 17
        switch ($this->getChar()) {
48
49 17
            case 'i':
50 5
                ++$this->offset;
51
52 5
                return $this->decodeInteger();
53
54 12
            case 'l':
55 2
                ++$this->offset;
56
57 2
                return $this->decodeList();
58
59 12
            case 'd':
60 4
                ++$this->offset;
61
62 4
                return $this->decodeDict();
63
64
            default:
65 11
                if (ctype_digit($this->getChar())) {
66 10
                    return $this->decodeString();
67
                }
68
69
        }
70
71 1
        throw new InvalidSourceException("Unknown entity found at offset $this->offset");
72
    }
73
74 5
    private function decodeInteger(): int
75
    {
76 5
        $offsetOfE = strpos($this->source, 'e', $this->offset);
77 5
        if (false === $offsetOfE) {
78 1
            throw new InvalidSourceException("Unterminated integer entity at offset $this->offset");
79
        }
80
81 4
        $currentOffset = $this->offset;
82 4
        if ('-' === $this->getChar($currentOffset)) {
83 1
            ++$currentOffset;
84
        }
85
86 4
        if ($offsetOfE === $currentOffset) {
87 1
            throw new InvalidSourceException("Empty integer entity at offset $this->offset");
88
        }
89
90 3
        while ($currentOffset < $offsetOfE) {
91 3
            if (!ctype_digit($this->getChar($currentOffset))) {
92 1
                throw new InvalidSourceException(
93 1
                    "Non-numeric character found in integer entity at offset $this->offset"
94
                );
95
            }
96 2
            ++$currentOffset;
97
        }
98
99 2
        $value = substr($this->source, $this->offset, $offsetOfE - $this->offset);
100
101 2
        $absoluteValue = (string)abs($value);
102 2
        if (1 < strlen($absoluteValue) && '0' === $value[0]) {
103 1
            throw new InvalidSourceException("Illegal zero-padding found in integer entity at offset $this->offset");
104
        }
105
106 1
        $this->offset = $offsetOfE + 1;
107
108 1
        return (int)$value + 0;
109
    }
110
111 10
    private function decodeString(): string
112
    {
113 10
        if ('0' === $this->getChar() && ':' !== $this->getChar($this->offset + 1)) {
114 1
            throw new InvalidSourceException(
115 1
                "Illegal zero-padding in string entity length declaration at offset $this->offset"
116
            );
117
        }
118
119 9
        $offsetOfColon = strpos($this->source, ':', $this->offset);
120 9
        if (false === $offsetOfColon) {
121 1
            throw new InvalidSourceException("Unterminated string entity at offset $this->offset");
122
        }
123
124 8
        $contentLength = (int)substr($this->source, $this->offset, $offsetOfColon);
125 8
        if (($contentLength + $offsetOfColon + 1) > $this->sourceLength) {
126 1
            throw new InvalidSourceException("Unexpected end of string entity at offset $this->offset");
127
        }
128
129 7
        $value        = substr($this->source, $offsetOfColon + 1, $contentLength);
130 7
        $this->offset = $offsetOfColon + $contentLength + 1;
131
132 7
        return $value;
133
    }
134
135 2
    private function decodeList(): array
136
    {
137 2
        $list       = [];
138 2
        $terminated = false;
139 2
        $listOffset = $this->offset;
140
141 2
        while (null !== $this->getChar()) {
142 2
            if ('e' === $this->getChar()) {
143 1
                $terminated = true;
144 1
                break;
145
            }
146
147 2
            $list[] = $this->doDecode();
148
        }
149
150 2
        if (false === $terminated && null === $this->getChar()) {
151 1
            throw new InvalidSourceException("Unterminated list definition at offset $listOffset");
152
        }
153
154 1
        $this->offset++;
155
156 1
        return $list;
157
    }
158
159 4
    private function decodeDict(): array
160
    {
161 4
        $dict       = [];
162 4
        $terminated = false;
163 4
        $dictOffset = $this->offset;
164
165 4
        while (null !== $this->getChar()) {
166 4
            if ('e' === $this->getChar()) {
167 1
                $terminated = true;
168 1
                break;
169
            }
170
171 4
            $keyOffset = $this->offset;
172 4
            if (false === ctype_digit($this->getChar())) {
173 1
                throw new InvalidSourceException("Invalid dictionary key at offset $keyOffset");
174
            }
175
176 3
            $key = $this->decodeString();
177 3
            if (isset($dict[$key])) {
178 1
                throw new InvalidSourceException("Duplicate dictionary key at offset $keyOffset");
179
            }
180
181 3
            $dict[$key] = $this->doDecode();
182
        }
183
184 2
        if (false === $terminated && null === $this->getChar()) {
185 1
            throw new InvalidSourceException("Unterminated dictionary definition at offset $dictOffset");
186
        }
187
188 1
        $this->offset++;
189
190 1
        return $dict;
191
    }
192
193 17
    private function getChar(int $offset = null): ?string
194
    {
195 17
        if (null === $offset) {
196 17
            $offset = $this->offset;
197
        }
198
199 17
        if (empty($this->source) || $this->offset >= $this->sourceLength) {
200 2
            return null;
201
        }
202
203 17
        return $this->source[$offset];
204
    }
205
}