1: | <?php |
2: | |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | namespace GameQ; |
22: | |
23: | use GameQ\Exception\Protocol as Exception; |
24: | |
25: | |
26: | |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | |
34: | |
35: | |
36: | class Buffer |
37: | { |
38: | |
39: | |
40: | |
41: | const NUMBER_TYPE_BIGENDIAN = 'be', |
42: | NUMBER_TYPE_LITTLEENDIAN = 'le', |
43: | NUMBER_TYPE_MACHINE = 'm'; |
44: | |
45: | |
46: | |
47: | |
48: | |
49: | |
50: | private $number_type = self::NUMBER_TYPE_LITTLEENDIAN; |
51: | |
52: | |
53: | |
54: | |
55: | |
56: | |
57: | private $data; |
58: | |
59: | |
60: | |
61: | |
62: | |
63: | |
64: | private $length; |
65: | |
66: | |
67: | |
68: | |
69: | |
70: | |
71: | private $index = 0; |
72: | |
73: | |
74: | |
75: | |
76: | |
77: | |
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: | |
88: | |
89: | |
90: | |
91: | public function getData() |
92: | { |
93: | return $this->data; |
94: | } |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | |
101: | public function getBuffer() |
102: | { |
103: | return substr($this->data, $this->index); |
104: | } |
105: | |
106: | |
107: | |
108: | |
109: | |
110: | |
111: | public function getLength() |
112: | { |
113: | return max($this->length - $this->index, 0); |
114: | } |
115: | |
116: | |
117: | |
118: | |
119: | |
120: | |
121: | |
122: | |
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: | |
138: | |
139: | |
140: | |
141: | |
142: | |
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: | |
156: | |
157: | |
158: | |
159: | |
160: | |
161: | public function lookAhead($length = 1) |
162: | { |
163: | return substr($this->data, $this->index, $length); |
164: | } |
165: | |
166: | |
167: | |
168: | |
169: | |
170: | |
171: | public function skip($length = 1) |
172: | { |
173: | $this->index += $length; |
174: | } |
175: | |
176: | |
177: | |
178: | |
179: | |
180: | |
181: | |
182: | public function jumpto($index) |
183: | { |
184: | $this->index = min($index, $this->length - 1); |
185: | } |
186: | |
187: | |
188: | |
189: | |
190: | |
191: | |
192: | public function getPosition() |
193: | { |
194: | return $this->index; |
195: | } |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | |
206: | |
207: | public function readString($delim = "\x00") |
208: | { |
209: | |
210: | $len = strpos($this->data, $delim, min($this->index, $this->length)); |
211: | |
212: | |
213: | if ($len === false) { |
214: | return $this->read(strlen($this->data) - $this->index); |
215: | } |
216: | |
217: | |
218: | $string = $this->read($len - $this->index); |
219: | ++$this->index; |
220: | |
221: | return $string; |
222: | } |
223: | |
224: | |
225: | |
226: | |
227: | |
228: | |
229: | |
230: | |
231: | |
232: | |
233: | public function readPascalString($offset = 0, $read_offset = false) |
234: | { |
235: | |
236: | $len = $this->readInt8(); |
237: | $offset = max($len - $offset, 0); |
238: | |
239: | |
240: | if ($read_offset) { |
241: | return $this->read($offset); |
242: | } else { |
243: | return substr($this->read($len), 0, $offset); |
244: | } |
245: | } |
246: | |
247: | |
248: | |
249: | |
250: | |
251: | |
252: | |
253: | |
254: | |
255: | |
256: | |
257: | |
258: | |
259: | |
260: | public function readStringMulti($delims, &$delimfound = null) |
261: | { |
262: | |
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: | |
271: | if (empty($pos)) { |
272: | return $this->read(strlen($this->data) - $this->index); |
273: | } |
274: | |
275: | |
276: | sort($pos); |
277: | $string = $this->read($pos[0] - $this->index); |
278: | $delimfound = $this->read(); |
279: | |
280: | return $string; |
281: | } |
282: | |
283: | |
284: | |
285: | |
286: | |
287: | |
288: | |
289: | public function readInt8() |
290: | { |
291: | $int = unpack('Cint', $this->read(1)); |
292: | |
293: | return $int['int']; |
294: | } |
295: | |
296: | |
297: | |
298: | |
299: | |
300: | |
301: | |
302: | public function readInt8Signed() |
303: | { |
304: | $int = unpack('cint', $this->read(1)); |
305: | |
306: | return $int['int']; |
307: | } |
308: | |
309: | |
310: | |
311: | |
312: | |
313: | |
314: | |
315: | public function readInt16() |
316: | { |
317: | |
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: | |
338: | |
339: | |
340: | |
341: | |
342: | public function readInt16Signed() |
343: | { |
344: | |
345: | $string = $this->read(2); |
346: | |
347: | |
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: | |
361: | |
362: | |
363: | |
364: | |
365: | public function readInt32($length = 4) |
366: | { |
367: | |
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: | |
385: | $corrected = $this->read($length); |
386: | |
387: | |
388: | $int = unpack($type . 'int', self::extendBinaryString($corrected, 4, $littleEndian)); |
389: | |
390: | return $int['int']; |
391: | } |
392: | |
393: | |
394: | |
395: | |
396: | |
397: | |
398: | |
399: | public function readInt32Signed() |
400: | { |
401: | |
402: | $string = $this->read(4); |
403: | |
404: | |
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: | |
418: | |
419: | |
420: | |
421: | |
422: | public function readInt64() |
423: | { |
424: | |
425: | if (version_compare(PHP_VERSION, '5.6.3') >= 0 && PHP_INT_SIZE == 8) { |
426: | |
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: | |
455: | $int = ($high << 32) | $low; |
456: | |
457: | unset($low, $high); |
458: | } |
459: | |
460: | return $int; |
461: | } |
462: | |
463: | |
464: | |
465: | |
466: | |
467: | |
468: | |
469: | public function readFloat32() |
470: | { |
471: | |
472: | $string = $this->read(4); |
473: | |
474: | |
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: | |