Buffer.php 12 KB

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