forked from rchouinard/bencode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDecoder.php
More file actions
298 lines (249 loc) · 8.52 KB
/
Copy pathDecoder.php
File metadata and controls
298 lines (249 loc) · 8.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
<?php
/**
* Power2All Bencode
*
* Bencode serializer for PHP 5.3+.
*
* @package Power2All\Bencode
* @copyright Copyright (c) 2014, Ryan Chouinard, modded by Power2All
* @author Ryan Chouinard <rchouinard@gmail.com>, Power2All <info@power2all.com>
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
*/
namespace Power2All\Bencode;
use Power2All\Bencode\Exception\RuntimeException;
/**
* Bencode decoder class
*
* Decodes bencode encoded strings.
*/
class Decoder
{
/**
* The encoded source string
*
* @var string
*/
private $source;
/**
* The length of the encoded source string
*
* @var integer
*/
private $sourceLength;
/**
* The return type for the decoded value
*
* @var Bencode::TYPE_ARRAY|Bencode::TYPE_OBJECT
*/
private $decodeType;
/**
* The current offset of the parser.
*
* @var integer
*/
private $offset = 0;
/**
* Decoder constructor
*
* @param string $source The bencode encoded source.
* @param string $decodeType Flag used to indicate whether the decoded
* value should be returned as an object or an array.
* @return void
*/
private function __construct($source, $decodeType)
{
$this->source = $source;
$this->sourceLength = strlen($this->source);
$this->decodeType = in_array($decodeType, array(Bencode::TYPE_ARRAY, Bencode::TYPE_OBJECT))
? $decodeType
: Bencode::TYPE_ARRAY;
}
/**
* Decode a bencode encoded string
*
* @param string $source The string to decode.
* @param string $decodeType Flag used to indicate whether the decoded
* value should be returned as an object or an array.
* @return mixed Returns the appropriate data type for the decoded data.
* @throws RuntimeException
*/
public static function decode($source, $decodeType = Bencode::TYPE_ARRAY)
{
try {
if (!is_string($source)) {
throw new RuntimeException("Argument expected to be a string; Got " . gettype($source));
}
$decoder = new self($source, $decodeType);
$decoded = $decoder->doDecode();
if ($decoder->offset != $decoder->sourceLength) {
throw new RuntimeException("Found multiple entities outside list or dict definitions");
}
return $decoded;
} catch (Exception $e) {
throw new RuntimeException("Unexpected Error: " . $e->getMessage());
}
}
/**
* Iterate over encoded entities in the source string and decode them
*
* @return mixed Returns the decoded value.
* @throws RuntimeException
*/
private function doDecode()
{
switch ($this->getChar()) {
case "i":
++$this->offset;
return $this->decodeInteger();
case "l":
++$this->offset;
return $this->decodeList();
case "d":
++$this->offset;
return $this->decodeDict();
default:
$character = $this->getChar();
if (is_string($character)) {
if (ctype_digit($character)) {
return $this->decodeString();
}
}
}
throw new RuntimeException("Unknown entity found at offset $this->offset");
}
/**
* Decode a bencode encoded integer
*
* @return integer Returns the decoded integer.
* @throws RuntimeException
*/
private function decodeInteger()
{
$offsetOfE = strpos($this->source, "e", $this->offset);
if (false === $offsetOfE) {
throw new RuntimeException("Unterminated integer entity at offset $this->offset");
}
$currentOffset = $this->offset;
if ("-" == $this->getChar($currentOffset)) {
++$currentOffset;
}
if ($offsetOfE === $currentOffset) {
throw new RuntimeException("Empty integer entity at offset $this->offset");
}
while ($currentOffset < $offsetOfE) {
if (!ctype_digit($this->getChar($currentOffset))) {
throw new RuntimeException("Non-numeric character found in integer entity at offset $this->offset");
}
++$currentOffset;
}
$value = substr($this->source, $this->offset, $offsetOfE - $this->offset);
// One last check to make sure zero-padded integers don't slip by, as
// they're not allowed per bencode specification.
$absoluteValue = (string) abs($value);
if (1 < strlen($absoluteValue) && "0" == $value[0]) {
throw new RuntimeException("Illegal zero-padding found in integer entity at offset $this->offset");
}
$this->offset = $offsetOfE + 1;
// The +0 auto-casts the chunk to either an integer or a float(in cases
// where an integer would overrun the max limits of integer types)
return $value + 0;
}
/**
* Decode a bencode encoded string
*
* @return string Returns the decoded string.
* @throws RuntimeException
*/
private function decodeString()
{
if ("0" === $this->getChar() && ":" != $this->getChar($this->offset + 1)) {
throw new RuntimeException("Illegal zero-padding in string entity length declaration at offset $this->offset");
}
$offsetOfColon = strpos($this->source, ":", $this->offset);
if (false === $offsetOfColon) {
throw new RuntimeException("Unterminated string entity at offset $this->offset");
}
$contentLength = (int) substr($this->source, $this->offset, $offsetOfColon);
if (($contentLength + $offsetOfColon + 1) > $this->sourceLength) {
throw new RuntimeException("Unexpected end of string entity at offset $this->offset");
}
$value = substr($this->source, $offsetOfColon + 1, $contentLength);
$this->offset = $offsetOfColon + $contentLength + 1;
return $value;
}
/**
* Decode a bencode encoded list
*
* @return array Returns the decoded array.
* @throws RuntimeException
*/
private function decodeList()
{
$list = array();
$terminated = false;
$listOffset = $this->offset;
while (false !== $this->getChar()) {
if ("e" == $this->getChar()) {
$terminated = true;
break;
}
$list[] = $this->doDecode();
}
if (!$terminated && false === $this->getChar()) {
throw new RuntimeException("Unterminated list definition at offset $listOffset");
}
$this->offset++;
return $list;
}
/**
* Decode a bencode encoded dictionary
*
* @return array Returns the decoded array.
* @throws RuntimeException
*/
private function decodeDict()
{
$dict = array();
$terminated = false;
$dictOffset = $this->offset;
while (false !== $this->getChar()) {
if ("e" == $this->getChar()) {
$terminated = true;
break;
}
$keyOffset = $this->offset;
if (!ctype_digit($this->getChar())) {
throw new RuntimeException("Invalid dictionary key at offset $keyOffset");
}
$key = $this->decodeString();
if (isset ($dict[$key])) {
throw new RuntimeException("Duplicate dictionary key at offset $keyOffset");
}
$dict[$key] = $this->doDecode();
}
if (!$terminated && false === $this->getChar()) {
throw new RuntimeException("Unterminated dictionary definition at offset $dictOffset");
}
$this->offset++;
return $dict;
}
/**
* Fetch the character at the specified source offset
*
* If offset is not provided, the current offset is used.
*
* @param integer $offset The offset to retrieve from the source string.
* @return string|false Returns the character found at the specified
* offset. If the specified offset is out of range, FALSE is returned.
*/
private function getChar($offset = null)
{
if (null === $offset) {
$offset = $this->offset;
}
if (empty ($this->source) || $this->offset >= $this->sourceLength) {
return false;
}
return $this->source[$offset];
}
}