Completed
Push — Exceptions ( bd05bd )
by Josh
03:16
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\IllegalCharacter;
15
use s9e\Bencode\Exceptions\PrematureEndOfData;
16
17
class Decoder
18
{
19
	/**
20
	* @var string Bencoded string being decoded
21
	*/
22
	protected string $bencoded;
23
24
	/**
25
	* @var int Length of the bencoded string
26
	*/
27
	protected int $len;
28
29
	/**
30
	* @var int Safe rightmost boundary
31
	*/
32
	protected int $max;
33
34
	/**
35
	* @var int Position of the cursor while decoding
36
	*/
37
	protected int $offset;
38
39 61
	public static function decode(string $bencoded)
40
	{
41 61
		$decoder = new static($bencoded);
42 50
		$value   = $decoder->decodeAnything();
43
44 26
		$decoder->checkCursorPosition();
45
46 18
		return $value;
47
	}
48
49 61
	protected function __construct(string $bencoded)
50
	{
51 61
		$this->bencoded = $bencoded;
52 61
		$this->len      = strlen($bencoded);
53 61
		$this->offset   = 0;
54
55 61
		if ($bencoded === '')
56
		{
57 1
			$this->prematureEndOfDataError();
58
		}
59 60
		$this->computeSafeBoundary();
60
	}
61
62 60
	protected function checkBoundary(): void
63
	{
64 60
		if ($this->max < 1)
65
		{
66 10
			if (strpos('-e', $this->bencoded[0]) !== false)
67
			{
68 2
				throw new IllegalCharacter(0);
69
			}
70
71 8
			$this->prematureEndOfDataError();
72
		}
73
	}
74
75
	/**
76
	* Check the cursor's position after decoding is done
77
	*/
78 26
	protected function checkCursorPosition(): void
79
	{
80 26
		if ($this->offset !== $this->len)
81
		{
82 8
			if ($this->offset > $this->len)
83
			{
84 3
				$this->prematureEndOfDataError();
85
			}
86
87 5
			$this->complianceError($this->offset, 'Superfluous content');
88
		}
89
	}
90
91 12
	protected function complianceError(int $pos, string $message = 'Illegal character'): void
92
	{
93 12
		throw new ComplianceError($pos, $message);
94
	}
95
96
	/**
97
	* Adjust the rightmost boundary to the last safe character that can start a value
98
	*
99
	* Will rewind the boundary to skip the rightmost digits, optionally preceded by "i" or "i-"
100
	*/
101 60
	protected function computeSafeBoundary(): void
102
	{
103 60
		$boundary = $this->len - 1;
104 60
		$c = $this->bencoded[$boundary];
105 60
		while (is_numeric($c) && --$boundary >= 0)
106
		{
107 7
			$c = $this->bencoded[$boundary];
108
		}
109 60
		if ($c === '-')
110
		{
111 6
			$boundary -= 2;
112
		}
113 54
		elseif ($c === 'i')
114
		{
115 6
			--$boundary;
116
		}
117
118 60
		$this->max = $boundary;
119 60
		$this->checkBoundary();
120
	}
121
122 50
	protected function decodeAnything()
123
	{
124 50
		$c = $this->bencoded[$this->offset];
125 50
		if ($c === 'i')
126
		{
127 17
			return $this->decodeInteger();
128
		}
129 41
		if ($c === 'd')
130
		{
131 15
			return $this->decodeDictionary();
132
		}
133 32
		if ($c === 'l')
134
		{
135 12
			return $this->decodeList();
136
		}
137
138 25
		return $this->decodeString();
139
	}
140
141 15
	protected function decodeDictionary(): ArrayObject
142
	{
143 15
		$values  = [];
144 15
		$lastKey = null;
145
146 15
		++$this->offset;
147 15
		while ($this->offset <= $this->max)
148
		{
149 15
			if ($this->bencoded[$this->offset] === 'e')
150
			{
151 7
				++$this->offset;
152
153 7
				return new ArrayObject($values, ArrayObject::ARRAY_AS_PROPS);
154
			}
155
156 14
			$offset = $this->offset;
157 14
			$key    = $this->decodeString();
158 13
			if ($key <= $lastKey)
159
			{
160 4
				$this->dictionaryComplianceError($offset, $key, $lastKey);
161
			}
162 13
			if ($this->offset > $this->max)
163
			{
164 1
				break;
165
			}
166 12
			$values[$key] = $this->decodeAnything();
167 9
			$lastKey      = $key;
168
		}
169
170 1
		$this->prematureEndOfDataError();
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return ArrayObject. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
171
	}
172
173 43
	protected function decodeDigits(string $terminator): int
174
	{
175
		// Digits sorted by decreasing frequency as observed on a random sample of torrent files
176 43
		$spn = strspn($this->bencoded, '4615302879', $this->offset);
177 43
		if (!$spn)
178
		{
179 7
			throw new IllegalCharacter($this->offset);
180
		}
181 38
		if ($this->bencoded[$this->offset] === '0' && $spn > 1)
182
		{
183 3
			$this->complianceError(1 + $this->offset);
184
		}
185
186
		// Capture the value and cast it as an integer
187 35
		$value = (int) substr($this->bencoded, $this->offset, $spn);
188
189 35
		$this->offset += $spn;
190 35
		if ($this->bencoded[$this->offset] !== $terminator)
191
		{
192 5
			throw new IllegalCharacter($this->offset);
193
		}
194 30
		++$this->offset;
195
196 30
		return $value;
197
	}
198
199 17
	protected function decodeInteger(): int
200
	{
201 17
		$negative = ($this->bencoded[++$this->offset] === '-');
202 17
		if ($negative && $this->bencoded[++$this->offset] === '0')
203
		{
204 1
			$this->complianceError($this->offset);
205
		}
206
207 16
		$value = $this->decodeDigits('e');
208
209 11
		return ($negative) ? -$value : $value;
210
	}
211
212 12
	protected function decodeList(): array
213
	{
214 12
		++$this->offset;
215
216 12
		$list = [];
217 12
		while ($this->offset <= $this->max)
218
		{
219 11
			if ($this->bencoded[$this->offset] === 'e')
220
			{
221 7
				++$this->offset;
222
223 7
				return $list;
224
			}
225
226 8
			$list[] = $this->decodeAnything();
227
		}
228
229 4
		$this->prematureEndOfDataError();
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return array. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
230
	}
231
232 34
	protected function decodeString(): string
233
	{
234 34
		$len           = $this->decodeDigits(':');
235 26
		$string        = substr($this->bencoded, $this->offset, $len);
236 26
		$this->offset += $len;
237
238 26
		return $string;
239
	}
240
241 4
	protected function dictionaryComplianceError(int $offset, string $key, ?string $lastKey): void
242
	{
243 4
		if ($key === $lastKey)
244
		{
245 2
			$this->complianceError($offset, "Duplicate dictionary entry '" . $key . "'");
246
		}
247 2
		elseif ($key < $lastKey)
248
		{
249 1
			$this->complianceError($offset, "Out of order dictionary entry '" . $key . "'");
250
		}
251
	}
252
253 17
	protected function prematureEndOfDataError()
254
	{
255 17
		throw new PrematureEndOfData(max(0, $this->len - 1));
256
	}
257
}