Passed
Push — v3 ( dc00fa...37cdd6 )
by
unknown
02:41
created

Buffer::getBuffer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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