Buffer::jumpto()   A
last analyzed

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 1
Bugs 1 Features 0
Metric Value
eloc 1
c 1
b 1
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * This file is part of GameQ.
4
 *
5
 * GameQ is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU Lesser General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * GameQ is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Lesser General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Lesser General Public License
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 *
18
 *
19
 */
20
21
namespace GameQ;
22
23
use GameQ\Exception\Protocol as Exception;
24
25
/**
26
 * Class Buffer
27
 *
28
 * Read specific byte sequences from a provided string or Buffer
29
 *
30
 * @package GameQ
31
 *
32
 * @author  Austin Bischoff <[email protected]>
33
 * @author  Aidan Lister <[email protected]>
34
 * @author  Tom Buskens <[email protected]>
35
 */
36
class Buffer
37
{
38
    /**
39
     * Constants for the byte code types we need to read as
40
     */
41
    const NUMBER_TYPE_BIGENDIAN = 'be',
42
        NUMBER_TYPE_LITTLEENDIAN = 'le',
43
        NUMBER_TYPE_MACHINE = 'm';
44
45
    /**
46
     * The number type we use for reading integers.  Defaults to little endian
47
     *
48
     * @var string
49
     */
50
    private $number_type = self::NUMBER_TYPE_LITTLEENDIAN;
51
52
    /**
53
     * The original data
54
     *
55
     * @var string
56
     */
57
    private $data;
58
59
    /**
60
     * The original data
61
     *
62
     * @var int
63
     */
64
    private $length;
65
66
    /**
67
     * Position of pointer
68
     *
69
     * @var int
70
     */
71
    private $index = 0;
72
73
    /**
74
     * Constructor
75
     *
76
     * @param string $data
77
     * @param string $number_type
78
     */
79 2076
    public function __construct($data, $number_type = self::NUMBER_TYPE_LITTLEENDIAN)
80
    {
81 2076
        $this->number_type = $number_type;
82 2076
        $this->data = $data;
83 2076
        $this->length = strlen($data);
84
    }
85
86
    /**
87
     * Return all the data
88
     *
89
     * @return  string    The data
90
     */
91 12
    public function getData()
92
    {
93 12
        return $this->data;
94
    }
95
96
    /**
97
     * Return data currently in the buffer
98
     *
99
     * @return  string    The data currently in the buffer
100
     */
101 1488
    public function getBuffer()
102
    {
103 1488
        return substr($this->data, $this->index);
104
    }
105
106
    /**
107
     * Returns the number of bytes in the buffer
108
     *
109
     * @return  int  Length of the buffer
110
     */
111 1404
    public function getLength()
112
    {
113 1404
        return max($this->length - $this->index, 0);
114
    }
115
116
    /**
117
     * Read from the buffer
118
     *
119
     * @param int $length
120
     *
121
     * @return string
122
     * @throws \GameQ\Exception\Protocol
123
     */
124 1992
    public function read($length = 1)
125
    {
126 1992
        if (($length + $this->index) > $this->length) {
127 12
            throw new Exception("Unable to read length={$length} from buffer.  Bad protocol format or return?");
128
        }
129
130 1980
        $string = substr($this->data, $this->index, $length);
131 1980
        $this->index += $length;
132
133 1980
        return $string;
134
    }
135
136
    /**
137
     * Read the last character from the buffer
138
     *
139
     * Unlike the other read functions, this function actually removes
140
     * the character from the buffer.
141
     *
142
     * @return string
143
     */
144 6
    public function readLast()
145
    {
146 6
        $len = strlen($this->data);
147 6
        $string = $this->data[strlen($this->data) - 1];
148 6
        $this->data = substr($this->data, 0, $len - 1);
149 6
        $this->length -= 1;
150
151 6
        return $string;
152
    }
153
154
    /**
155
     * Look at the buffer, but don't remove
156
     *
157
     * @param int $length
158
     *
159
     * @return string
160
     */
161 1104
    public function lookAhead($length = 1)
162
    {
163 1104
        return substr($this->data, $this->index, $length);
164
    }
165
166
    /**
167
     * Skip forward in the buffer
168
     *
169
     * @param int $length
170
     */
171 486
    public function skip($length = 1)
172
    {
173 486
        $this->index += $length;
174
    }
175
176
    /**
177
     * Jump to a specific position in the buffer,
178
     * will not jump past end of buffer
179
     *
180
     * @param $index
181
     */
182 54
    public function jumpto($index)
183
    {
184 54
        $this->index = min($index, $this->length - 1);
185
    }
186
187
    /**
188
     * Get the current pointer position
189
     *
190
     * @return int
191
     */
192 6
    public function getPosition()
193
    {
194 6
        return $this->index;
195
    }
196
197
    /**
198
     * Read from buffer until delimiter is reached
199
     *
200
     * If not found, return everything
201
     *
202
     * @param string $delim
203
     *
204
     * @return string
205
     * @throws \GameQ\Exception\Protocol
206
     */
207 1482
    public function readString($delim = "\x00")
208
    {
209
        // Get position of delimiter
210 1482
        $len = strpos($this->data, $delim, min($this->index, $this->length));
211
212
        // If it is not found then return whole buffer
213 1482
        if ($len === false) {
214 180
            return $this->read(strlen($this->data) - $this->index);
215
        }
216
217
        // Read the string and remove the delimiter
218 1482
        $string = $this->read($len - $this->index);
219 1482
        ++$this->index;
220
221 1482
        return $string;
222
    }
223
224
    /**
225
     * Reads a pascal string from the buffer
226
     *
227
     * @param int  $offset      Number of bits to cut off the end
228
     * @param bool $read_offset True if the data after the offset is to be read
229
     *
230
     * @return string
231
     * @throws \GameQ\Exception\Protocol
232
     */
233 78
    public function readPascalString($offset = 0, $read_offset = false)
234
    {
235
        // Get the proper offset
236 78
        $len = $this->readInt8();
237 78
        $offset = max($len - $offset, 0);
238
239
        // Read the data
240 78
        if ($read_offset) {
241 36
            return $this->read($offset);
242
        } else {
243 42
            return substr($this->read($len), 0, $offset);
244
        }
245
    }
246
247
    /**
248
     * Read from buffer until any of the delimiters is reached
249
     *
250
     * If not found, return everything
251
     *
252
     * @param              $delims
253
     * @param null|string &$delimfound
254
     *
255
     * @return string
256
     * @throws \GameQ\Exception\Protocol
257
     *
258
     * @todo: Check to see if this is even used anymore
259
     */
260 204
    public function readStringMulti($delims, &$delimfound = null)
261
    {
262
        // Get position of delimiters
263 204
        $pos = [];
264 204
        foreach ($delims as $delim) {
265 204
            if ($index = strpos($this->data, $delim, min($this->index, $this->length))) {
266 204
                $pos[] = $index;
267
            }
268
        }
269
270
        // If none are found then return whole buffer
271 204
        if (empty($pos)) {
272 204
            return $this->read(strlen($this->data) - $this->index);
273
        }
274
275
        // Read the string and remove the delimiter
276 204
        sort($pos);
277 204
        $string = $this->read($pos[0] - $this->index);
278 204
        $delimfound = $this->read();
279
280 204
        return $string;
281
    }
282
283
    /**
284
     * Read an 8-bit unsigned integer
285
     *
286
     * @return int
287
     * @throws \GameQ\Exception\Protocol
288
     */
289 1182
    public function readInt8()
290
    {
291 1182
        $int = unpack('Cint', $this->read(1));
292
293 1182
        return $int['int'];
294
    }
295
296
    /**
297
     * Read and 8-bit signed integer
298
     *
299
     * @return int
300
     * @throws \GameQ\Exception\Protocol
301
     */
302 30
    public function readInt8Signed()
303
    {
304 30
        $int = unpack('cint', $this->read(1));
305
306 30
        return $int['int'];
307
    }
308
309
    /**
310
     * Read a 16-bit unsigned integer
311
     *
312
     * @return int
313
     * @throws \GameQ\Exception\Protocol
314
     */
315 930
    public function readInt16()
316
    {
317
        // Change the integer type we are looking up
318 930
        switch ($this->number_type) {
319 310
            case self::NUMBER_TYPE_BIGENDIAN:
320 30
                $type = 'nint';
321 30
                break;
322
323 300
            case self::NUMBER_TYPE_LITTLEENDIAN:
324 888
                $type = 'vint';
325 888
                break;
326
327
            default:
328 12
                $type = 'Sint';
329
        }
330
331 930
        $int = unpack($type, $this->read(2));
332
333 930
        return $int['int'];
334
    }
335
336
    /**
337
     * Read a 16-bit signed integer
338
     *
339
     * @return int
340
     * @throws \GameQ\Exception\Protocol
341
     */
342 882
    public function readInt16Signed()
343
    {
344
        // Read the data into a string
345 882
        $string = $this->read(2);
346
347
        // For big endian we need to reverse the bytes
348 882
        if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) {
349 18
            $string = strrev($string);
350
        }
351
352 882
        $int = unpack('sint', $string);
353
354 882
        unset($string);
355
356 882
        return $int['int'];
357
    }
358
359
    /**
360
     * Read a 32-bit unsigned integer
361
     *
362
     * @return int
363
     * @throws \GameQ\Exception\Protocol
364
     */
365 330
    public function readInt32($length = 4)
366
    {
367
        // Change the integer type we are looking up
368 330
        $littleEndian = null;
369 330
        switch ($this->number_type) {
370 110
            case self::NUMBER_TYPE_BIGENDIAN:
371 144
                $type = 'N';
372 144
                $littleEndian = false;
373 144
                break;
374
375 62
            case self::NUMBER_TYPE_LITTLEENDIAN:
376 174
                $type = 'V';
377 174
                $littleEndian = true;
378 174
                break;
379
380
            default:
381 12
                $type = 'L';
382
        }
383
384
        // read from the buffer and append/prepend empty bytes for shortened int32
385 330
        $corrected = $this->read($length);
386
387
        // Unpack the number
388 330
        $int = unpack($type . 'int', self::extendBinaryString($corrected, 4, $littleEndian));
389
390 330
        return $int['int'];
391
    }
392
393
    /**
394
     * Read a 32-bit signed integer
395
     *
396
     * @return int
397
     * @throws \GameQ\Exception\Protocol
398
     */
399 900
    public function readInt32Signed()
400
    {
401
        // Read the data into a string
402 900
        $string = $this->read(4);
403
404
        // For big endian we need to reverse the bytes
405 900
        if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) {
406 18
            $string = strrev($string);
407
        }
408
409 900
        $int = unpack('lint', $string);
410
411 900
        unset($string);
412
413 900
        return $int['int'];
414
    }
415
416
    /**
417
     * Read a 64-bit unsigned integer
418
     *
419
     * @return int
420
     * @throws \GameQ\Exception\Protocol
421
     */
422 858
    public function readInt64()
423
    {
424
        // We have the pack 64-bit codes available. See: http://php.net/manual/en/function.pack.php
425 858
        if (version_compare(PHP_VERSION, '5.6.3') >= 0 && PHP_INT_SIZE == 8) {
426
            // Change the integer type we are looking up
427 858
            switch ($this->number_type) {
428 286
                case self::NUMBER_TYPE_BIGENDIAN:
429 18
                    $type = 'Jint';
430 18
                    break;
431
432 280
                case self::NUMBER_TYPE_LITTLEENDIAN:
433 828
                    $type = 'Pint';
434 828
                    break;
435
436
                default:
437 12
                    $type = 'Qint';
438
            }
439
440 858
            $int64 = unpack($type, $this->read(8));
441
442 858
            $int = $int64['int'];
443
444 858
            unset($int64);
445
        } else {
446
            if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) {
447
                $high = $this->readInt32();
448
                $low = $this->readInt32();
449
            } else {
450
                $low = $this->readInt32();
451
                $high = $this->readInt32();
452
            }
453
454
            // We have to determine the number via bitwise
455
            $int = ($high << 32) | $low;
456
457
            unset($low, $high);
458
        }
459
460 858
        return $int;
461
    }
462
463
    /**
464
     * Read a 32-bit float
465
     *
466
     * @return float
467
     * @throws \GameQ\Exception\Protocol
468
     */
469 624
    public function readFloat32()
470
    {
471
        // Read the data into a string
472 624
        $string = $this->read(4);
473
474
        // For big endian we need to reverse the bytes
475 624
        if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) {
476 18
            $string = strrev($string);
477
        }
478
479 624
        $float = unpack('ffloat', $string);
480
481 624
        unset($string);
482
483 624
        return $float['float'];
484
    }
485
486 330
    private static function extendBinaryString($input, $length = 4, $littleEndian = null)
487
    {
488 330
        if (is_null($littleEndian)) {
489 12
            $littleEndian = self::isLittleEndian();
490
        }
491
492 330
        $extension = str_repeat(pack($littleEndian ? 'V' : 'N', 0b0000), $length - strlen($input));
493
494 330
        if ($littleEndian) {
495 186
            return $input . $extension;
496
        } else {
497 144
            return $extension . $input;
498
        }
499
    }
500
501 12
    private static function isLittleEndian()
502
    {
503 12
        return 0x00FF === current(unpack('v', pack('S', 0x00FF)));
504
    }
505
}
506