Completed
Push — Exceptions ( 69c4e2...90da39 )
by Josh
02:15
created

Decoder::prematureEndOfDataError()   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
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
* @package   s9e\Bencode
5
* @copyright Copyright (c) 2014-2020 The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\Bencode;
9
10
use ArrayObject;
11
use InvalidArgumentException;
12
use RuntimeException;
13
use s9e\Bencode\Exceptions\ComplianceError;
14
use s9e\Bencode\Exceptions\DecodingException;
15
16
class Decoder
17
{
18
	/**
19
	* @var string Bencoded string being decoded
20
	*/
21
	protected string $bencoded;
22
23
	/**
24
	* @var int Length of the bencoded string
25
	*/
26
	protected int $len;
27
28
	/**
29
	* @var int Safe rightmost boundary
30
	*/
31
	protected int $max;
32
33
	/**
34
	* @var int Position of the cursor while decoding
35
	*/
36
	protected int $offset;
37
38 61
	public static function decode(string $bencoded)
39
	{
40 61
		$decoder = new static($bencoded);
41 50
		$value   = $decoder->decodeAnything();
42
43 26
		$decoder->checkCursorPosition();
44
45 18
		return $value;
46
	}
47
48 61
	protected function __construct(string $bencoded)
49
	{
50 61
		$this->bencoded = $bencoded;
51 61
		$this->len      = strlen($bencoded);
52 61
		$this->offset   = 0;
53
54 61
		if ($bencoded === '')
55
		{
56 1
			throw new DecodingException('Premature end of data', 0);
57
		}
58 60
		$this->computeSafeBoundary();
59
	}
60
61 60
	protected function checkBoundary(): void
62
	{
63 60
		if ($this->max < 1)
64
		{
65 10
			if (strpos('-e', $this->bencoded[0]) !== false)
66
			{
67 2
				throw new DecodingException('Illegal character', 0);
68
			}
69
70 8
			throw new DecodingException('Premature end of data', $this->len - 1);
71
		}
72
	}
73
74
	/**
75
	* Check the cursor's position after decoding is done
76
	*/
77 26
	protected function checkCursorPosition(): void
78
	{
79 26
		if ($this->offset !== $this->len)
80
		{
81 8
			if ($this->offset > $this->len)
82
			{
83 3
				throw new DecodingException('Premature end of data', $this->len - 1);
84
			}
85
86 5
			$this->complianceError('Superfluous content', $this->offset);
87
		}
88
	}
89
90 12
	protected function complianceError(string $message, int $offset): void
91
	{
92 12
		throw new ComplianceError($message, $offset);
93
	}
94
95
	/**
96
	* Adjust the rightmost boundary to the last safe character that can start a value
97
	*
98
	* Will rewind the boundary to skip the rightmost digits, optionally preceded by "i" or "i-"
99
	*/
100 60
	protected function computeSafeBoundary(): void
101
	{
102 60
		$boundary = $this->len - 1;
103 60
		$c = $this->bencoded[$boundary];
104 60
		while (is_numeric($c) && --$boundary >= 0)
105
		{
106 7
			$c = $this->bencoded[$boundary];
107
		}
108 60
		if ($c === '-')
109
		{
110 6
			$boundary -= 2;
111
		}
112 54
		elseif ($c === 'i')
113
		{
114 6
			--$boundary;
115
		}
116
117 60
		$this->max = $boundary;
118 60
		$this->checkBoundary();
119
	}
120
121 50
	protected function decodeAnything()
122
	{
123 50
		$c = $this->bencoded[$this->offset];
124 50
		if ($c === 'i')
125
		{
126 17
			return $this->decodeInteger();
127
		}
128 41
		if ($c === 'd')
129
		{
130 15
			return $this->decodeDictionary();
131
		}
132 32
		if ($c === 'l')
133
		{
134 12
			return $this->decodeList();
135
		}
136
137 25
		return $this->decodeString();
138
	}
139
140 15
	protected function decodeDictionary(): ArrayObject
141
	{
142 15
		$values  = [];
143 15
		$lastKey = null;
144
145 15
		++$this->offset;
146 15
		while ($this->offset <= $this->max)
147
		{
148 15
			if ($this->bencoded[$this->offset] === 'e')
149
			{
150 7
				++$this->offset;
151
152 7
				return new ArrayObject($values, ArrayObject::ARRAY_AS_PROPS);
153
			}
154
155 14
			$offset = $this->offset;
156 14
			$key    = $this->decodeString();
157 13
			if ($key <= $lastKey)
158
			{
159 4
				$this->dictionaryComplianceError($offset, $key, $lastKey);
160
			}
161 13
			if ($this->offset > $this->max)
162
			{
163 1
				break;
164
			}
165 12
			$values[$key] = $this->decodeAnything();
166 9
			$lastKey      = $key;
167
		}
168
169 1
		throw new DecodingException('Premature end of data', $this->len - 1);
170
	}
171
172 43
	protected function decodeDigits(string $terminator): int
173
	{
174
		// Digits sorted by decreasing frequency as observed on a random sample of torrent files
175 43
		$spn = strspn($this->bencoded, '4615302879', $this->offset);
176 43
		if (!$spn)
177
		{
178 7
			throw new DecodingException('Illegal character', $this->offset);
179
		}
180 38
		if ($this->bencoded[$this->offset] === '0' && $spn > 1)
181
		{
182 3
			$this->complianceError('Illegal character', 1 + $this->offset);
183
		}
184
185
		// Capture the value and cast it as an integer
186 35
		$value = (int) substr($this->bencoded, $this->offset, $spn);
187
188 35
		$this->offset += $spn;
189 35
		if ($this->bencoded[$this->offset] !== $terminator)
190
		{
191 5
			throw new DecodingException('Illegal character', $this->offset);
192
		}
193 30
		++$this->offset;
194
195 30
		return $value;
196
	}
197
198 17
	protected function decodeInteger(): int
199
	{
200 17
		$negative = ($this->bencoded[++$this->offset] === '-');
201 17
		if ($negative && $this->bencoded[++$this->offset] === '0')
202
		{
203 1
			$this->complianceError('Illegal character', $this->offset);
204
		}
205
206 16
		$value = $this->decodeDigits('e');
207
208 11
		return ($negative) ? -$value : $value;
209
	}
210
211 12
	protected function decodeList(): array
212
	{
213 12
		++$this->offset;
214
215 12
		$list = [];
216 12
		while ($this->offset <= $this->max)
217
		{
218 11
			if ($this->bencoded[$this->offset] === 'e')
219
			{
220 7
				++$this->offset;
221
222 7
				return $list;
223
			}
224
225 8
			$list[] = $this->decodeAnything();
226
		}
227
228 4
		throw new DecodingException('Premature end of data', $this->len - 1);
229
	}
230
231 34
	protected function decodeString(): string
232
	{
233 34
		$len           = $this->decodeDigits(':');
234 26
		$string        = substr($this->bencoded, $this->offset, $len);
235 26
		$this->offset += $len;
236
237 26
		return $string;
238
	}
239
240 4
	protected function dictionaryComplianceError(int $offset, string $key, ?string $lastKey): void
241
	{
242 4
		if ($key === $lastKey)
243
		{
244 2
			$this->complianceError("Duplicate dictionary entry '" . $key . "'", $offset);
245
		}
246 2
		elseif ($key < $lastKey)
247
		{
248 1
			$this->complianceError("Out of order dictionary entry '" . $key . "'", $offset);
249
		}
250
	}
251
}