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 <austin@codebeard.com>
33: * @author Aidan Lister <aidan@php.net>
34: * @author Tom Buskens <t.buskens@deviation.nl>
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: public function __construct($data, $number_type = self::NUMBER_TYPE_LITTLEENDIAN)
80: {
81: $this->number_type = $number_type;
82: $this->data = $data;
83: $this->length = strlen($data);
84: }
85:
86: /**
87: * Return all the data
88: *
89: * @return string The data
90: */
91: public function getData()
92: {
93: 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: public function getBuffer()
102: {
103: 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: public function getLength()
112: {
113: 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: public function read($length = 1)
125: {
126: if (($length + $this->index) > $this->length) {
127: throw new Exception("Unable to read length={$length} from buffer. Bad protocol format or return?");
128: }
129:
130: $string = substr($this->data, $this->index, $length);
131: $this->index += $length;
132:
133: 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: public function readLast()
145: {
146: $len = strlen($this->data);
147: $string = $this->data[strlen($this->data) - 1];
148: $this->data = substr($this->data, 0, $len - 1);
149: $this->length -= 1;
150:
151: 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: public function lookAhead($length = 1)
162: {
163: return substr($this->data, $this->index, $length);
164: }
165:
166: /**
167: * Skip forward in the buffer
168: *
169: * @param int $length
170: */
171: public function skip($length = 1)
172: {
173: $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: public function jumpto($index)
183: {
184: $this->index = min($index, $this->length - 1);
185: }
186:
187: /**
188: * Get the current pointer position
189: *
190: * @return int
191: */
192: public function getPosition()
193: {
194: 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: public function readString($delim = "\x00")
208: {
209: // Get position of delimiter
210: $len = strpos($this->data, $delim, min($this->index, $this->length));
211:
212: // If it is not found then return whole buffer
213: if ($len === false) {
214: return $this->read(strlen($this->data) - $this->index);
215: }
216:
217: // Read the string and remove the delimiter
218: $string = $this->read($len - $this->index);
219: ++$this->index;
220:
221: 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: public function readPascalString($offset = 0, $read_offset = false)
234: {
235: // Get the proper offset
236: $len = $this->readInt8();
237: $offset = max($len - $offset, 0);
238:
239: // Read the data
240: if ($read_offset) {
241: return $this->read($offset);
242: } else {
243: 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: public function readStringMulti($delims, &$delimfound = null)
261: {
262: // Get position of delimiters
263: $pos = [];
264: foreach ($delims as $delim) {
265: if ($index = strpos($this->data, $delim, min($this->index, $this->length))) {
266: $pos[] = $index;
267: }
268: }
269:
270: // If none are found then return whole buffer
271: if (empty($pos)) {
272: return $this->read(strlen($this->data) - $this->index);
273: }
274:
275: // Read the string and remove the delimiter
276: sort($pos);
277: $string = $this->read($pos[0] - $this->index);
278: $delimfound = $this->read();
279:
280: return $string;
281: }
282:
283: /**
284: * Read an 8-bit unsigned integer
285: *
286: * @return int
287: * @throws \GameQ\Exception\Protocol
288: */
289: public function readInt8()
290: {
291: $int = unpack('Cint', $this->read(1));
292:
293: return $int['int'];
294: }
295:
296: /**
297: * Read and 8-bit signed integer
298: *
299: * @return int
300: * @throws \GameQ\Exception\Protocol
301: */
302: public function readInt8Signed()
303: {
304: $int = unpack('cint', $this->read(1));
305:
306: return $int['int'];
307: }
308:
309: /**
310: * Read a 16-bit unsigned integer
311: *
312: * @return int
313: * @throws \GameQ\Exception\Protocol
314: */
315: public function readInt16()
316: {
317: // Change the integer type we are looking up
318: switch ($this->number_type) {
319: case self::NUMBER_TYPE_BIGENDIAN:
320: $type = 'nint';
321: break;
322:
323: case self::NUMBER_TYPE_LITTLEENDIAN:
324: $type = 'vint';
325: break;
326:
327: default:
328: $type = 'Sint';
329: }
330:
331: $int = unpack($type, $this->read(2));
332:
333: return $int['int'];
334: }
335:
336: /**
337: * Read a 16-bit signed integer
338: *
339: * @return int
340: * @throws \GameQ\Exception\Protocol
341: */
342: public function readInt16Signed()
343: {
344: // Read the data into a string
345: $string = $this->read(2);
346:
347: // For big endian we need to reverse the bytes
348: if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) {
349: $string = strrev($string);
350: }
351:
352: $int = unpack('sint', $string);
353:
354: unset($string);
355:
356: return $int['int'];
357: }
358:
359: /**
360: * Read a 32-bit unsigned integer
361: *
362: * @return int
363: * @throws \GameQ\Exception\Protocol
364: */
365: public function readInt32($length = 4)
366: {
367: // Change the integer type we are looking up
368: $littleEndian = null;
369: switch ($this->number_type) {
370: case self::NUMBER_TYPE_BIGENDIAN:
371: $type = 'N';
372: $littleEndian = false;
373: break;
374:
375: case self::NUMBER_TYPE_LITTLEENDIAN:
376: $type = 'V';
377: $littleEndian = true;
378: break;
379:
380: default:
381: $type = 'L';
382: }
383:
384: // read from the buffer and append/prepend empty bytes for shortened int32
385: $corrected = $this->read($length);
386:
387: // Unpack the number
388: $int = unpack($type . 'int', self::extendBinaryString($corrected, 4, $littleEndian));
389:
390: return $int['int'];
391: }
392:
393: /**
394: * Read a 32-bit signed integer
395: *
396: * @return int
397: * @throws \GameQ\Exception\Protocol
398: */
399: public function readInt32Signed()
400: {
401: // Read the data into a string
402: $string = $this->read(4);
403:
404: // For big endian we need to reverse the bytes
405: if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) {
406: $string = strrev($string);
407: }
408:
409: $int = unpack('lint', $string);
410:
411: unset($string);
412:
413: return $int['int'];
414: }
415:
416: /**
417: * Read a 64-bit unsigned integer
418: *
419: * @return int
420: * @throws \GameQ\Exception\Protocol
421: */
422: public function readInt64()
423: {
424: // We have the pack 64-bit codes available. See: http://php.net/manual/en/function.pack.php
425: if (version_compare(PHP_VERSION, '5.6.3') >= 0 && PHP_INT_SIZE == 8) {
426: // Change the integer type we are looking up
427: switch ($this->number_type) {
428: case self::NUMBER_TYPE_BIGENDIAN:
429: $type = 'Jint';
430: break;
431:
432: case self::NUMBER_TYPE_LITTLEENDIAN:
433: $type = 'Pint';
434: break;
435:
436: default:
437: $type = 'Qint';
438: }
439:
440: $int64 = unpack($type, $this->read(8));
441:
442: $int = $int64['int'];
443:
444: 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: return $int;
461: }
462:
463: /**
464: * Read a 32-bit float
465: *
466: * @return float
467: * @throws \GameQ\Exception\Protocol
468: */
469: public function readFloat32()
470: {
471: // Read the data into a string
472: $string = $this->read(4);
473:
474: // For big endian we need to reverse the bytes
475: if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) {
476: $string = strrev($string);
477: }
478:
479: $float = unpack('ffloat', $string);
480:
481: unset($string);
482:
483: return $float['float'];
484: }
485:
486: private static function extendBinaryString($input, $length = 4, $littleEndian = null)
487: {
488: if (is_null($littleEndian)) {
489: $littleEndian = self::isLittleEndian();
490: }
491:
492: $extension = str_repeat(pack($littleEndian ? 'V' : 'N', 0b0000), $length - strlen($input));
493:
494: if ($littleEndian) {
495: return $input . $extension;
496: } else {
497: return $extension . $input;
498: }
499: }
500:
501: private static function isLittleEndian()
502: {
503: return 0x00FF === current(unpack('v', pack('S', 0x00FF)));
504: }
505: }
506: