Passed
Push — master ( 2c49eb...da29c3 )
by Josh
07:38
created

Decoder::checkDictionaryCompliance()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

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