Completed
Branch master (411689)
by Josh
02:00
created

Decoder::checkCursorPosition()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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