Completed
Pull Request — master (#28)
by Maxime
01:54
created

BitManipulation::bytesFromToString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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