1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is a part of Woketo package. |
5
|
|
|
* |
6
|
|
|
* (c) Nekland <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full license, take a look to the LICENSE file |
9
|
|
|
* on the root directory of this project |
10
|
|
|
*/ |
11
|
|
|
declare(strict_types=1); |
12
|
|
|
|
13
|
|
|
namespace Nekland\Woketo\Utils; |
14
|
|
|
use Nekland\Woketo\Exception\Utils\NotLongEnoughException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Class BitManipulation |
18
|
|
|
* |
19
|
|
|
* Glossary: |
20
|
|
|
* - in this context a "frame" is an assembly of bytes represented by a "byte-string" or a (signed) int. |
21
|
|
|
*/ |
22
|
|
|
class BitManipulation |
23
|
|
|
{ |
24
|
|
|
/** |
25
|
|
|
* Mode from to is the default mode of inspection of frames. But PHP usually uses from and length to inspect frames. |
26
|
|
|
*/ |
27
|
|
|
const MODE_FROM_TO = 0; |
28
|
|
|
const MODE_PHP = 1; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Get a specific bit from a byte. |
32
|
|
|
* |
33
|
|
|
* @param int $byte |
34
|
|
|
* @param int $bitNumber |
35
|
|
|
* @return int |
36
|
|
|
*/ |
37
|
81 |
|
public static function nthBit(int $byte, int $bitNumber) : int |
38
|
|
|
{ |
39
|
81 |
|
if ($byte < 0 || $byte > 255) { |
40
|
2 |
|
throw new \InvalidArgumentException( |
41
|
2 |
|
\sprintf('The given integer %s is not a byte.', $byte) |
42
|
|
|
); |
43
|
|
|
} |
44
|
|
|
|
45
|
79 |
|
if ($bitNumber < 1 || $bitNumber > 8) { |
46
|
2 |
|
throw new \InvalidArgumentException( |
47
|
2 |
|
\sprintf('The bit number %s is not a correct value for a byte (1-8 required).', $bitNumber) |
48
|
|
|
); |
49
|
|
|
} |
50
|
|
|
|
51
|
77 |
|
$realNth = \pow(2, 8 - $bitNumber); |
52
|
|
|
|
53
|
77 |
|
return (int) ($realNth === ($byte & $realNth)); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Get a specific byte inside a frame represented by an int or a string. |
58
|
|
|
* |
59
|
|
|
* @param string|int $frame Non utf8 string (this should be more precisely a bytes-string). |
60
|
|
|
* @param int $byteNumber Starting at 0. |
61
|
|
|
* @return int |
62
|
|
|
*/ |
63
|
88 |
|
public static function nthByte($frame, int $byteNumber) : int |
64
|
|
|
{ |
65
|
88 |
|
if (\is_string($frame)) { |
66
|
78 |
|
$len = BitManipulation::frameSize($frame); |
67
|
|
|
|
68
|
78 |
|
if ($byteNumber < 0 || $byteNumber > ($len-1)) { |
69
|
2 |
|
throw new \InvalidArgumentException( |
70
|
2 |
|
\sprintf('The frame is only %s bytes larges but you tried to get the %sth byte.', $len, $byteNumber) |
71
|
|
|
); |
72
|
|
|
} |
73
|
|
|
|
74
|
76 |
|
return \ord($frame[$byteNumber]); |
75
|
|
|
} |
76
|
|
|
|
77
|
10 |
|
if (\is_int($frame)) { |
78
|
9 |
|
if ($frame < 0) { |
79
|
1 |
|
throw new \InvalidArgumentException( |
80
|
1 |
|
\sprintf('This method does not support negative ints as parameter for now. %s given.', $byteNumber) |
81
|
|
|
); |
82
|
|
|
} |
83
|
8 |
|
$hex = \dechex($frame); |
84
|
8 |
|
$len = BitManipulation::frameSize($hex); |
85
|
|
|
|
86
|
|
|
// Index of the first octal of the wanted byte |
87
|
8 |
|
$realByteNth = $byteNumber * 2; |
88
|
|
|
|
89
|
8 |
|
if ($byteNumber < 0 || ($realByteNth + 1) > $len) { |
90
|
3 |
|
throw new \InvalidArgumentException( |
91
|
3 |
|
\sprintf('Impossible to get the byte %s from the frame %s.', $byteNumber, $frame) |
92
|
|
|
); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
// Considering FF12AB (number) if you want the byte represented by AB you need to get the |
96
|
|
|
// first letter, shift it of 4 and add the next letter. |
97
|
|
|
// This may seems weird but that's because you read numbers from right to left. |
98
|
6 |
|
return (\hexdec($hex[$realByteNth]) << 4) + \hexdec($hex[$realByteNth + 1]); |
99
|
|
|
// _Notice that if the number is from right to left, your data is still from left to right_ |
100
|
|
|
} |
101
|
|
|
|
102
|
1 |
|
throw new \InvalidArgumentException( |
103
|
1 |
|
\sprintf('The frame must be an int or string, %s given.', gettype($frame)) |
104
|
|
|
); |
105
|
|
|
} |
106
|
|
|
|
107
|
74 |
|
public static function partOfByte(int $byte, int $part) : int |
108
|
|
|
{ |
109
|
74 |
|
if ($byte < 0 || $byte > 255) { |
110
|
|
|
throw new \InvalidArgumentException(sprintf('%s is not a byte', $byte)); |
111
|
|
|
} |
112
|
|
|
|
113
|
74 |
|
if ($part === 1) { |
114
|
1 |
|
return ($byte & 240) >> 4; |
115
|
|
|
} |
116
|
|
|
|
117
|
73 |
|
if ($part === 2) { |
118
|
72 |
|
return $byte & 15; |
119
|
|
|
} |
120
|
|
|
|
121
|
1 |
|
throw new \InvalidArgumentException(sprintf('A byte have only 2 parts. %s asked.', $part)); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Because strings are the best way to store many bytes in PHP it can |
126
|
|
|
* be useful to make the conversion between hex (which are strings) |
127
|
|
|
* array to string. |
128
|
|
|
* |
129
|
|
|
* @param array $hexArray |
130
|
|
|
* @return string |
131
|
|
|
*/ |
132
|
55 |
|
public static function hexArrayToString(...$hexArray) : string |
133
|
|
|
{ |
134
|
55 |
|
if (\is_array($hexArray[0])) { |
135
|
36 |
|
$hexArray = $hexArray[0]; |
136
|
|
|
} |
137
|
|
|
|
138
|
55 |
|
$res = ''; |
139
|
55 |
|
foreach ($hexArray as $hexNum) { |
140
|
55 |
|
$res .= \chr(\hexdec($hexNum)); |
141
|
|
|
} |
142
|
|
|
|
143
|
55 |
|
return $res; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* @param string|int $frame |
148
|
|
|
* @param int $from Byte where to start (should be inferior to $to). |
149
|
|
|
* @param int $to Byte where to stop (considering it starts at 0). The `to` value include the target |
150
|
|
|
* byte. |
151
|
|
|
* @param bool $force8bytes By default PHP have a wrong behavior with 8 bytes variables. If you have 8 bytes |
152
|
|
|
* the returned int will be negative (because unsigned integers does not exists in PHP) |
153
|
|
|
* @return int |
154
|
|
|
*/ |
155
|
39 |
|
public static function bytesFromTo($frame, int $from, int $to, bool $force8bytes = false) : int |
156
|
|
|
{ |
157
|
|
|
// No more than 64b (which return negative number when the first bit is specified) |
158
|
39 |
|
if (($to - $from) > 7 && (!$force8bytes && ($to - $from) !== 8)) { |
159
|
|
|
if ($force8bytes) { |
160
|
|
|
throw new \InvalidArgumentException(sprintf('Not more than 8 bytes (64bit) is supported by this method and you asked for %s bytes.', $to - $from)); |
161
|
|
|
} |
162
|
|
|
throw new \InvalidArgumentException('PHP limitation: getting more than 7 bytes will return a negative number because unsigned int does not exist.'); |
163
|
|
|
} |
164
|
|
|
|
165
|
39 |
|
if (\is_string($frame)) { |
166
|
32 |
|
if ((BitManipulation::frameSize($frame) - 1) < $to) { |
167
|
2 |
|
throw new NotLongEnoughException('The frame is not long enough.'); |
168
|
|
|
} |
169
|
|
|
|
170
|
30 |
|
$subStringLength = $to - $from + 1; |
171
|
|
|
// Getting responsible bytes |
172
|
30 |
|
$subString = \substr($frame, $from, $subStringLength); |
173
|
30 |
|
$res = 0; |
174
|
|
|
|
175
|
|
|
// for each byte, getting ord |
176
|
30 |
|
for($i = 0; $i < $subStringLength; $i++) { |
177
|
30 |
|
$res <<= 8; |
178
|
30 |
|
$res += \ord($subString[$i]); |
179
|
|
|
} |
180
|
|
|
|
181
|
30 |
|
return $res; |
182
|
|
|
} |
183
|
|
|
|
184
|
7 |
|
if (!\is_int($frame)) { |
185
|
1 |
|
throw new \InvalidArgumentException( |
186
|
1 |
|
\sprintf('A frame can only be a string or int. %s given', gettype($frame)) |
187
|
|
|
); |
188
|
|
|
} |
189
|
|
|
|
190
|
6 |
|
if ($frame < 0) { |
191
|
1 |
|
throw new \InvalidArgumentException('The frame cannot be a negative number'); |
192
|
|
|
} |
193
|
|
|
|
194
|
5 |
|
$res = 0; |
195
|
5 |
|
for ($i = $from; $i <= $to; $i++) { |
196
|
5 |
|
$res <<= 8; |
197
|
5 |
|
$res += BitManipulation::nthByte($frame, $i); |
198
|
|
|
} |
199
|
|
|
|
200
|
3 |
|
return $res; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Proxy to the substr to be sure to be use the right method (mb_substr) |
205
|
|
|
* |
206
|
|
|
* @param string $frame |
207
|
|
|
* @param int $from |
208
|
|
|
* @param int $to |
209
|
|
|
* @return string |
210
|
|
|
*/ |
211
|
37 |
|
public static function bytesFromToString(string $frame, int $from, int $to, int $mode = BitManipulation::MODE_FROM_TO) : string |
212
|
|
|
{ |
213
|
37 |
|
if ($mode === BitManipulation::MODE_FROM_TO) { |
214
|
11 |
|
return \mb_substr($frame, $from, $to - $from + 1, '8bit'); |
215
|
|
|
} |
216
|
|
|
|
217
|
37 |
|
return \mb_substr($frame, $from, $to, '8bit'); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Take a frame represented by a decimal int to transform it in a string. |
222
|
|
|
* Notice that any int is a frame and cannot be more than 8 bytes |
223
|
|
|
* |
224
|
|
|
* @param int $frame |
225
|
|
|
* @param int|null $size In bytes. This value should always be precise. Be careful if you don't ! |
226
|
|
|
* @return string |
227
|
|
|
*/ |
228
|
23 |
|
public static function intToBinaryString(int $frame, int $size = null) : string |
229
|
|
|
{ |
230
|
23 |
|
$format = 'J*'; |
231
|
|
|
|
232
|
23 |
|
if ($size !== null) { |
233
|
|
|
switch (true) { |
234
|
17 |
|
case $size <= 2: |
235
|
7 |
|
$format = 'n*'; |
236
|
7 |
|
break; |
237
|
11 |
|
case $size <= 4: |
238
|
10 |
|
$format = 'N*'; |
239
|
10 |
|
break; |
240
|
1 |
|
case $size > 4: |
241
|
1 |
|
$format = 'J*'; |
242
|
1 |
|
break; |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
|
246
|
23 |
|
$res = \pack($format, $frame); |
247
|
|
|
|
248
|
23 |
|
if ($size === null) { |
249
|
9 |
|
$res = \ltrim($res, "\0"); |
250
|
|
|
} |
251
|
|
|
|
252
|
23 |
|
return $res; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Take an string frame and transform it to a decimal frame (inside an int). |
257
|
|
|
* |
258
|
|
|
* @param string $frame |
259
|
|
|
* @return int |
260
|
|
|
*/ |
261
|
2 |
|
public static function binaryStringtoInt(string $frame) : int |
262
|
|
|
{ |
263
|
2 |
|
$len = BitManipulation::frameSize($frame); |
264
|
|
|
|
265
|
2 |
|
if ($len > 8) { |
266
|
|
|
throw new \InvalidArgumentException( |
267
|
|
|
\sprintf('The string %s cannot be converted to int because an int cannot be more than 8 bytes (64b).', $frame) |
268
|
|
|
); |
269
|
|
|
} |
270
|
|
|
|
271
|
2 |
|
if (\in_array(BitManipulation::frameSize($frame), [1, 3])) { |
272
|
1 |
|
$frame = "\0" . $frame; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
switch(true) { |
276
|
2 |
|
case $len <= 2: |
277
|
1 |
|
$format = 'n'; |
278
|
1 |
|
break; |
279
|
2 |
|
case $len <= 4: |
280
|
2 |
|
$format = 'N'; |
281
|
2 |
|
break; |
282
|
|
|
default: // also known as "$len > 4" :) |
|
|
|
|
283
|
1 |
|
$format = 'J'; |
284
|
|
|
|
285
|
|
|
do { |
286
|
1 |
|
$frame = "\0" . $frame; |
287
|
1 |
|
} while (BitManipulation::frameSize($frame) !== 8); |
288
|
|
|
} |
289
|
|
|
|
290
|
2 |
|
return \unpack($format, $frame)[1]; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Method that return frame as hex (more readable). |
295
|
|
|
* Helpful for debug ! |
296
|
|
|
* |
297
|
|
|
* @param string $frame |
298
|
|
|
* @return string |
299
|
|
|
*/ |
300
|
3 |
|
public static function binaryStringToHex(string $frame) : string |
301
|
|
|
{ |
302
|
3 |
|
return \unpack('H*', $frame)[1]; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Haters gonna hate. `strlen` cannot be trusted because of an option of mbstring extension, more info: |
307
|
|
|
* http://php.net/manual/fr/mbstring.overload.php |
308
|
|
|
* http://php.net/manual/fr/function.mb-strlen.php#77040 |
309
|
|
|
* |
310
|
|
|
* @param string $frame |
311
|
|
|
* @return int |
312
|
|
|
*/ |
313
|
101 |
|
public static function frameSize(string $frame) : int |
314
|
|
|
{ |
315
|
101 |
|
if (\extension_loaded('mbstring')) { |
316
|
101 |
|
return \mb_strlen($frame, '8bit'); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
return \strlen($frame); |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.