├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpstan.neon.dist ├── src ├── Binary.php ├── BinaryDataException.php ├── BinaryStream.php └── Limits.php └── tests └── phpunit └── BinaryTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = yes 3 | 4 | [*] 5 | indent_size = 4 6 | indent_style = tab 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.php text eol=lf 4 | *.sh text eol=lf 5 | *.txt text eol=lf 6 | *.properties text eol=lf 7 | *.bat text eol=crlf 8 | *.cmd text eol=crlf 9 | *.ps1 text eol=crlf 10 | 11 | # Custom for Visual Studio 12 | *.cs diff=csharp 13 | *.sln merge=union 14 | *.csproj merge=union 15 | *.vbproj merge=union 16 | *.fsproj merge=union 17 | *.dbproj merge=union 18 | 19 | # Standard to msysgit 20 | *.doc diff=astextplain 21 | *.DOC diff=astextplain 22 | *.docx diff=astextplain 23 | *.DOCX diff=astextplain 24 | *.dot diff=astextplain 25 | *.DOT diff=astextplain 26 | *.pdf diff=astextplain 27 | *.PDF diff=astextplain 28 | *.rtf diff=astextplain 29 | *.RTF diff=astextplain 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: monthly 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, '[ci skip]')" 9 | strategy: 10 | matrix: 11 | php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] 12 | name: PHP ${{ matrix.php }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup PHP 16 | uses: shivammathur/setup-php@2.30.0 17 | with: 18 | php-version: ${{ matrix.php }} 19 | - name: Cache Composer packages 20 | id: composer-cache 21 | uses: actions/cache@v4 22 | with: 23 | path: "~/.composer/cache" 24 | key: "php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}" 25 | restore-keys: "php-${{ matrix.php }}-composer-" 26 | - name: Install dependencies 27 | run: composer install --prefer-dist --no-interaction 28 | - name: Run PHPStan 29 | run: ./vendor/bin/phpstan analyze --no-progress 30 | - name: Run PHPUnit 31 | run: ./vendor/bin/phpunit tests/phpunit --fail-on-warning 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | phpstan.neon 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PocketMine-BinaryUtils 2 | ![CI](https://github.com/pmmp/BinaryUtils/workflows/CI/badge.svg) 3 | 4 | Classes and methods for conveniently handling binary data 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pocketmine/binaryutils", 3 | "description": "Classes and methods for conveniently handling binary data", 4 | "type": "library", 5 | "license": "LGPL-3.0", 6 | "require": { 7 | "php": "^7.4 || ^8.0", 8 | "php-64bit": "*" 9 | }, 10 | "require-dev": { 11 | "phpstan/phpstan": "2.1.0", 12 | "phpstan/extension-installer": "^1.0", 13 | "phpstan/phpstan-strict-rules": "^2.0.0", 14 | "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0", 15 | "phpstan/phpstan-phpunit": "^2.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "pocketmine\\utils\\": "src/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "pocketmine\\utils\\": "tests/phpunit/" 25 | } 26 | }, 27 | "config": { 28 | "allow-plugins": { 29 | "phpstan/extension-installer": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src 5 | - tests/phpunit 6 | treatPhpDocTypesAsCertain: false 7 | ignoreErrors: 8 | - 9 | #this would only happen if the regex was broken 10 | message: "#^Method pocketmine\\\\utils\\\\Binary\\:\\:printFloat\\(\\) should return string but returns string\\|null\\.$#" 11 | count: 1 12 | path: src/Binary.php 13 | -------------------------------------------------------------------------------- /src/Binary.php: -------------------------------------------------------------------------------- 1 | > 56; 51 | } 52 | 53 | public static function unsignByte(int $value) : int{ 54 | return $value & 0xff; 55 | } 56 | 57 | public static function signShort(int $value) : int{ 58 | return $value << 48 >> 48; 59 | } 60 | 61 | public static function unsignShort(int $value) : int{ 62 | return $value & 0xffff; 63 | } 64 | 65 | public static function signInt(int $value) : int{ 66 | return $value << 32 >> 32; 67 | } 68 | 69 | public static function unsignInt(int $value) : int{ 70 | return $value & 0xffffffff; 71 | } 72 | 73 | public static function flipShortEndianness(int $value) : int{ 74 | return self::readLShort(self::writeShort($value)); 75 | } 76 | 77 | public static function flipIntEndianness(int $value) : int{ 78 | return self::readLInt(self::writeInt($value)); 79 | } 80 | 81 | public static function flipLongEndianness(int $value) : int{ 82 | return self::readLLong(self::writeLong($value)); 83 | } 84 | 85 | /** 86 | * @return mixed[] 87 | * @throws BinaryDataException 88 | */ 89 | private static function safeUnpack(string $formatCode, string $bytes, int $needLength) : array{ 90 | $haveLength = strlen($bytes); 91 | if($haveLength < $needLength){ 92 | throw new BinaryDataException("Not enough bytes: need $needLength, have $haveLength"); 93 | } 94 | //unpack SUCKS SO BADLY. We really need an extension to replace this garbage :( 95 | $result = unpack($formatCode, $bytes); 96 | if($result === false){ 97 | //this should never happen; we checked the length above 98 | throw new \AssertionError("unpack() failed for unknown reason"); 99 | } 100 | return $result; 101 | } 102 | 103 | /** 104 | * Reads a byte boolean 105 | */ 106 | public static function readBool(string $b) : bool{ 107 | return $b[0] !== "\x00"; 108 | } 109 | 110 | /** 111 | * Writes a byte boolean 112 | */ 113 | public static function writeBool(bool $b) : string{ 114 | return $b ? "\x01" : "\x00"; 115 | } 116 | 117 | /** 118 | * Reads an unsigned byte (0 - 255) 119 | * 120 | * @throws BinaryDataException 121 | */ 122 | public static function readByte(string $c) : int{ 123 | if($c === ""){ 124 | throw new BinaryDataException("Expected a string of length 1"); 125 | } 126 | return ord($c[0]); 127 | } 128 | 129 | /** 130 | * Reads a signed byte (-128 - 127) 131 | * 132 | * @throws BinaryDataException 133 | */ 134 | public static function readSignedByte(string $c) : int{ 135 | if($c === ""){ 136 | throw new BinaryDataException("Expected a string of length 1"); 137 | } 138 | return self::signByte(ord($c[0])); 139 | } 140 | 141 | /** 142 | * Writes an unsigned/signed byte 143 | */ 144 | public static function writeByte(int $c) : string{ 145 | return chr($c); 146 | } 147 | 148 | /** 149 | * Reads a 16-bit unsigned big-endian number 150 | * 151 | * @throws BinaryDataException 152 | */ 153 | public static function readShort(string $str) : int{ 154 | return self::safeUnpack("n", $str, self::SIZEOF_SHORT)[1]; 155 | } 156 | 157 | /** 158 | * Reads a 16-bit signed big-endian number 159 | * 160 | * @throws BinaryDataException 161 | */ 162 | public static function readSignedShort(string $str) : int{ 163 | return self::signShort(self::safeUnpack("n", $str, self::SIZEOF_SHORT)[1]); 164 | } 165 | 166 | /** 167 | * Writes a 16-bit signed/unsigned big-endian number 168 | */ 169 | public static function writeShort(int $value) : string{ 170 | return pack("n", $value); 171 | } 172 | 173 | /** 174 | * Reads a 16-bit unsigned little-endian number 175 | * 176 | * @throws BinaryDataException 177 | */ 178 | public static function readLShort(string $str) : int{ 179 | return self::safeUnpack("v", $str, self::SIZEOF_SHORT)[1]; 180 | } 181 | 182 | /** 183 | * Reads a 16-bit signed little-endian number 184 | * 185 | * @throws BinaryDataException 186 | */ 187 | public static function readSignedLShort(string $str) : int{ 188 | return self::signShort(self::safeUnpack("v", $str, self::SIZEOF_SHORT)[1]); 189 | } 190 | 191 | /** 192 | * Writes a 16-bit signed/unsigned little-endian number 193 | */ 194 | public static function writeLShort(int $value) : string{ 195 | return pack("v", $value); 196 | } 197 | 198 | /** 199 | * Reads a 3-byte big-endian number 200 | * 201 | * @throws BinaryDataException 202 | */ 203 | public static function readTriad(string $str) : int{ 204 | return self::safeUnpack("N", "\x00" . $str, self::SIZEOF_INT)[1]; 205 | } 206 | 207 | /** 208 | * Writes a 3-byte big-endian number 209 | */ 210 | public static function writeTriad(int $value) : string{ 211 | return substr(pack("N", $value), 1); 212 | } 213 | 214 | /** 215 | * Reads a 3-byte little-endian number 216 | * 217 | * @throws BinaryDataException 218 | */ 219 | public static function readLTriad(string $str) : int{ 220 | return self::safeUnpack("V", $str . "\x00", self::SIZEOF_INT)[1]; 221 | } 222 | 223 | /** 224 | * Writes a 3-byte little-endian number 225 | */ 226 | public static function writeLTriad(int $value) : string{ 227 | return substr(pack("V", $value), 0, -1); 228 | } 229 | 230 | /** 231 | * Reads a 4-byte signed integer 232 | * 233 | * @throws BinaryDataException 234 | */ 235 | public static function readInt(string $str) : int{ 236 | return self::signInt(self::safeUnpack("N", $str, self::SIZEOF_INT)[1]); 237 | } 238 | 239 | /** 240 | * Writes a 4-byte integer 241 | */ 242 | public static function writeInt(int $value) : string{ 243 | return pack("N", $value); 244 | } 245 | 246 | /** 247 | * Reads a 4-byte signed little-endian integer 248 | * 249 | * @throws BinaryDataException 250 | */ 251 | public static function readLInt(string $str) : int{ 252 | return self::signInt(self::safeUnpack("V", $str, self::SIZEOF_INT)[1]); 253 | } 254 | 255 | /** 256 | * Writes a 4-byte signed little-endian integer 257 | */ 258 | public static function writeLInt(int $value) : string{ 259 | return pack("V", $value); 260 | } 261 | 262 | /** 263 | * Reads a 4-byte floating-point number 264 | * 265 | * @throws BinaryDataException 266 | */ 267 | public static function readFloat(string $str) : float{ 268 | return self::safeUnpack("G", $str, self::SIZEOF_FLOAT)[1]; 269 | } 270 | 271 | /** 272 | * Reads a 4-byte floating-point number, rounded to the specified number of decimal places. 273 | * 274 | * @throws BinaryDataException 275 | */ 276 | public static function readRoundedFloat(string $str, int $accuracy) : float{ 277 | return round(self::readFloat($str), $accuracy); 278 | } 279 | 280 | /** 281 | * Writes a 4-byte floating-point number. 282 | */ 283 | public static function writeFloat(float $value) : string{ 284 | return pack("G", $value); 285 | } 286 | 287 | /** 288 | * Reads a 4-byte little-endian floating-point number. 289 | * 290 | * @throws BinaryDataException 291 | */ 292 | public static function readLFloat(string $str) : float{ 293 | return self::safeUnpack("g", $str, self::SIZEOF_FLOAT)[1]; 294 | } 295 | 296 | /** 297 | * Reads a 4-byte little-endian floating-point number rounded to the specified number of decimal places. 298 | * 299 | * @throws BinaryDataException 300 | */ 301 | public static function readRoundedLFloat(string $str, int $accuracy) : float{ 302 | return round(self::readLFloat($str), $accuracy); 303 | } 304 | 305 | /** 306 | * Writes a 4-byte little-endian floating-point number. 307 | */ 308 | public static function writeLFloat(float $value) : string{ 309 | return pack("g", $value); 310 | } 311 | 312 | /** 313 | * Returns a printable floating-point number. 314 | */ 315 | public static function printFloat(float $value) : string{ 316 | return preg_replace("/(\\.\\d+?)0+$/", "$1", sprintf("%F", $value)); 317 | } 318 | 319 | /** 320 | * Reads an 8-byte floating-point number. 321 | * 322 | * @throws BinaryDataException 323 | */ 324 | public static function readDouble(string $str) : float{ 325 | return self::safeUnpack("E", $str, self::SIZEOF_DOUBLE)[1]; 326 | } 327 | 328 | /** 329 | * Writes an 8-byte floating-point number. 330 | */ 331 | public static function writeDouble(float $value) : string{ 332 | return pack("E", $value); 333 | } 334 | 335 | /** 336 | * Reads an 8-byte little-endian floating-point number. 337 | * 338 | * @throws BinaryDataException 339 | */ 340 | public static function readLDouble(string $str) : float{ 341 | return self::safeUnpack("e", $str, self::SIZEOF_DOUBLE)[1]; 342 | } 343 | 344 | /** 345 | * Writes an 8-byte floating-point little-endian number. 346 | */ 347 | public static function writeLDouble(float $value) : string{ 348 | return pack("e", $value); 349 | } 350 | 351 | /** 352 | * Reads an 8-byte integer. 353 | * 354 | * @throws BinaryDataException 355 | */ 356 | public static function readLong(string $str) : int{ 357 | return self::safeUnpack("J", $str, self::SIZEOF_LONG)[1]; 358 | } 359 | 360 | /** 361 | * Writes an 8-byte integer. 362 | */ 363 | public static function writeLong(int $value) : string{ 364 | return pack("J", $value); 365 | } 366 | 367 | /** 368 | * Reads an 8-byte little-endian integer. 369 | * 370 | * @throws BinaryDataException 371 | */ 372 | public static function readLLong(string $str) : int{ 373 | return self::safeUnpack("P", $str, self::SIZEOF_LONG)[1]; 374 | } 375 | 376 | /** 377 | * Writes an 8-byte little-endian integer. 378 | */ 379 | public static function writeLLong(int $value) : string{ 380 | return pack("P", $value); 381 | } 382 | 383 | /** 384 | * Reads a 32-bit zigzag-encoded variable-length integer. 385 | * 386 | * @param int $offset reference parameter 387 | * 388 | * @throws BinaryDataException 389 | */ 390 | public static function readVarInt(string $buffer, int &$offset) : int{ 391 | $raw = self::readUnsignedVarInt($buffer, $offset); 392 | $temp = ((($raw << 63) >> 63) ^ $raw) >> 1; 393 | return $temp ^ ($raw & (1 << 63)); 394 | } 395 | 396 | /** 397 | * Reads a 32-bit variable-length unsigned integer. 398 | * 399 | * @param int $offset reference parameter 400 | * 401 | * @throws BinaryDataException if the var-int did not end after 5 bytes or there were not enough bytes 402 | */ 403 | public static function readUnsignedVarInt(string $buffer, int &$offset) : int{ 404 | $value = 0; 405 | for($i = 0; $i <= 28; $i += 7){ 406 | if(!isset($buffer[$offset])){ 407 | throw new BinaryDataException("No bytes left in buffer"); 408 | } 409 | $b = ord($buffer[$offset++]); 410 | $value |= (($b & 0x7f) << $i); 411 | 412 | if(($b & 0x80) === 0){ 413 | return $value; 414 | } 415 | } 416 | 417 | throw new BinaryDataException("VarInt did not terminate after 5 bytes!"); 418 | } 419 | 420 | /** 421 | * Writes a 32-bit integer as a zigzag-encoded variable-length integer. 422 | */ 423 | public static function writeVarInt(int $v) : string{ 424 | $v = ($v << 32 >> 32); 425 | return self::writeUnsignedVarInt(($v << 1) ^ ($v >> 31)); 426 | } 427 | 428 | /** 429 | * Writes a 32-bit unsigned integer as a variable-length integer. 430 | * 431 | * @return string up to 5 bytes 432 | */ 433 | public static function writeUnsignedVarInt(int $value) : string{ 434 | $buf = ""; 435 | $remaining = $value & 0xffffffff; 436 | for($i = 0; $i < 5; ++$i){ 437 | if(($remaining >> 7) !== 0){ 438 | $buf .= chr($remaining | 0x80); 439 | }else{ 440 | $buf .= chr($remaining & 0x7f); 441 | return $buf; 442 | } 443 | 444 | $remaining = (($remaining >> 7) & (PHP_INT_MAX >> 6)); //PHP really needs a logical right-shift operator 445 | } 446 | 447 | throw new InvalidArgumentException("Value too large to be encoded as a VarInt"); 448 | } 449 | 450 | /** 451 | * Reads a 64-bit zigzag-encoded variable-length integer. 452 | * 453 | * @param int $offset reference parameter 454 | * 455 | * @throws BinaryDataException 456 | */ 457 | public static function readVarLong(string $buffer, int &$offset) : int{ 458 | $raw = self::readUnsignedVarLong($buffer, $offset); 459 | $temp = ((($raw << 63) >> 63) ^ $raw) >> 1; 460 | return $temp ^ ($raw & (1 << 63)); 461 | } 462 | 463 | /** 464 | * Reads a 64-bit unsigned variable-length integer. 465 | * 466 | * @param int $offset reference parameter 467 | * 468 | * @throws BinaryDataException if the var-int did not end after 10 bytes or there were not enough bytes 469 | */ 470 | public static function readUnsignedVarLong(string $buffer, int &$offset) : int{ 471 | $value = 0; 472 | for($i = 0; $i <= 63; $i += 7){ 473 | if(!isset($buffer[$offset])){ 474 | throw new BinaryDataException("No bytes left in buffer"); 475 | } 476 | $b = ord($buffer[$offset++]); 477 | $value |= (($b & 0x7f) << $i); 478 | 479 | if(($b & 0x80) === 0){ 480 | return $value; 481 | } 482 | } 483 | 484 | throw new BinaryDataException("VarLong did not terminate after 10 bytes!"); 485 | } 486 | 487 | /** 488 | * Writes a 64-bit integer as a zigzag-encoded variable-length long. 489 | */ 490 | public static function writeVarLong(int $v) : string{ 491 | return self::writeUnsignedVarLong(($v << 1) ^ ($v >> 63)); 492 | } 493 | 494 | /** 495 | * Writes a 64-bit unsigned integer as a variable-length long. 496 | */ 497 | public static function writeUnsignedVarLong(int $value) : string{ 498 | $buf = ""; 499 | $remaining = $value; 500 | for($i = 0; $i < 10; ++$i){ 501 | if(($remaining >> 7) !== 0){ 502 | $buf .= chr($remaining | 0x80); //Let chr() take the last byte of this, it's faster than adding another & 0x7f. 503 | }else{ 504 | $buf .= chr($remaining & 0x7f); 505 | return $buf; 506 | } 507 | 508 | $remaining = (($remaining >> 7) & (PHP_INT_MAX >> 6)); //PHP really needs a logical right-shift operator 509 | } 510 | 511 | throw new InvalidArgumentException("Value too large to be encoded as a VarLong"); 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /src/BinaryDataException.php: -------------------------------------------------------------------------------- 1 | buffer = $buffer; 41 | $this->offset = $offset; 42 | } 43 | 44 | /** 45 | * Rewinds the stream pointer to the start. 46 | */ 47 | public function rewind() : void{ 48 | $this->offset = 0; 49 | } 50 | 51 | public function setOffset(int $offset) : void{ 52 | $this->offset = $offset; 53 | } 54 | 55 | public function getOffset() : int{ 56 | return $this->offset; 57 | } 58 | 59 | public function getBuffer() : string{ 60 | return $this->buffer; 61 | } 62 | 63 | /** 64 | * @phpstan-impure 65 | * @throws BinaryDataException if there are not enough bytes left in the buffer 66 | */ 67 | public function get(int $len) : string{ 68 | if($len === 0){ 69 | return ""; 70 | } 71 | if($len < 0){ 72 | throw new \InvalidArgumentException("Length must be positive"); 73 | } 74 | 75 | $remaining = strlen($this->buffer) - $this->offset; 76 | if($remaining < $len){ 77 | throw new BinaryDataException("Not enough bytes left in buffer: need $len, have $remaining"); 78 | } 79 | 80 | return $len === 1 ? $this->buffer[$this->offset++] : substr($this->buffer, ($this->offset += $len) - $len, $len); 81 | } 82 | 83 | /** 84 | * @phpstan-impure 85 | * @throws BinaryDataException 86 | */ 87 | public function getRemaining() : string{ 88 | $buflen = strlen($this->buffer); 89 | if($this->offset >= $buflen){ 90 | throw new BinaryDataException("No bytes left to read"); 91 | } 92 | $str = substr($this->buffer, $this->offset); 93 | $this->offset = $buflen; 94 | return $str; 95 | } 96 | 97 | public function put(string $str) : void{ 98 | $this->buffer .= $str; 99 | } 100 | 101 | /** 102 | * @phpstan-impure 103 | * @throws BinaryDataException 104 | */ 105 | public function getBool() : bool{ 106 | return $this->get(1) !== "\x00"; 107 | } 108 | 109 | public function putBool(bool $v) : void{ 110 | $this->buffer .= ($v ? "\x01" : "\x00"); 111 | } 112 | 113 | /** 114 | * @phpstan-impure 115 | * @throws BinaryDataException 116 | */ 117 | public function getByte() : int{ 118 | return ord($this->get(1)); 119 | } 120 | 121 | public function putByte(int $v) : void{ 122 | $this->buffer .= chr($v); 123 | } 124 | 125 | /** 126 | * @phpstan-impure 127 | * @throws BinaryDataException 128 | */ 129 | public function getShort() : int{ 130 | return Binary::readShort($this->get(2)); 131 | } 132 | 133 | /** 134 | * @phpstan-impure 135 | * @throws BinaryDataException 136 | */ 137 | public function getSignedShort() : int{ 138 | return Binary::readSignedShort($this->get(2)); 139 | } 140 | 141 | public function putShort(int $v) : void{ 142 | $this->buffer .= Binary::writeShort($v); 143 | } 144 | 145 | /** 146 | * @phpstan-impure 147 | * @throws BinaryDataException 148 | */ 149 | public function getLShort() : int{ 150 | return Binary::readLShort($this->get(2)); 151 | } 152 | 153 | /** 154 | * @phpstan-impure 155 | * @throws BinaryDataException 156 | */ 157 | public function getSignedLShort() : int{ 158 | return Binary::readSignedLShort($this->get(2)); 159 | } 160 | 161 | public function putLShort(int $v) : void{ 162 | $this->buffer .= Binary::writeLShort($v); 163 | } 164 | 165 | /** 166 | * @phpstan-impure 167 | * @throws BinaryDataException 168 | */ 169 | public function getTriad() : int{ 170 | return Binary::readTriad($this->get(3)); 171 | } 172 | 173 | public function putTriad(int $v) : void{ 174 | $this->buffer .= Binary::writeTriad($v); 175 | } 176 | 177 | /** 178 | * @phpstan-impure 179 | * @throws BinaryDataException 180 | */ 181 | public function getLTriad() : int{ 182 | return Binary::readLTriad($this->get(3)); 183 | } 184 | 185 | public function putLTriad(int $v) : void{ 186 | $this->buffer .= Binary::writeLTriad($v); 187 | } 188 | 189 | /** 190 | * @phpstan-impure 191 | * @throws BinaryDataException 192 | */ 193 | public function getInt() : int{ 194 | return Binary::readInt($this->get(4)); 195 | } 196 | 197 | public function putInt(int $v) : void{ 198 | $this->buffer .= Binary::writeInt($v); 199 | } 200 | 201 | /** 202 | * @phpstan-impure 203 | * @throws BinaryDataException 204 | */ 205 | public function getLInt() : int{ 206 | return Binary::readLInt($this->get(4)); 207 | } 208 | 209 | public function putLInt(int $v) : void{ 210 | $this->buffer .= Binary::writeLInt($v); 211 | } 212 | 213 | /** 214 | * @phpstan-impure 215 | * @throws BinaryDataException 216 | */ 217 | public function getFloat() : float{ 218 | return Binary::readFloat($this->get(4)); 219 | } 220 | 221 | /** 222 | * @phpstan-impure 223 | * @throws BinaryDataException 224 | */ 225 | public function getRoundedFloat(int $accuracy) : float{ 226 | return Binary::readRoundedFloat($this->get(4), $accuracy); 227 | } 228 | 229 | public function putFloat(float $v) : void{ 230 | $this->buffer .= Binary::writeFloat($v); 231 | } 232 | 233 | /** 234 | * @phpstan-impure 235 | * @throws BinaryDataException 236 | */ 237 | public function getLFloat() : float{ 238 | return Binary::readLFloat($this->get(4)); 239 | } 240 | 241 | /** 242 | * @phpstan-impure 243 | * @throws BinaryDataException 244 | */ 245 | public function getRoundedLFloat(int $accuracy) : float{ 246 | return Binary::readRoundedLFloat($this->get(4), $accuracy); 247 | } 248 | 249 | public function putLFloat(float $v) : void{ 250 | $this->buffer .= Binary::writeLFloat($v); 251 | } 252 | 253 | /** 254 | * @phpstan-impure 255 | * @throws BinaryDataException 256 | */ 257 | public function getDouble() : float{ 258 | return Binary::readDouble($this->get(8)); 259 | } 260 | 261 | public function putDouble(float $v) : void{ 262 | $this->buffer .= Binary::writeDouble($v); 263 | } 264 | 265 | /** 266 | * @phpstan-impure 267 | * @throws BinaryDataException 268 | */ 269 | public function getLDouble() : float{ 270 | return Binary::readLDouble($this->get(8)); 271 | } 272 | 273 | public function putLDouble(float $v) : void{ 274 | $this->buffer .= Binary::writeLDouble($v); 275 | } 276 | 277 | /** 278 | * @phpstan-impure 279 | * @throws BinaryDataException 280 | */ 281 | public function getLong() : int{ 282 | return Binary::readLong($this->get(8)); 283 | } 284 | 285 | public function putLong(int $v) : void{ 286 | $this->buffer .= Binary::writeLong($v); 287 | } 288 | 289 | /** 290 | * @phpstan-impure 291 | * @throws BinaryDataException 292 | */ 293 | public function getLLong() : int{ 294 | return Binary::readLLong($this->get(8)); 295 | } 296 | 297 | public function putLLong(int $v) : void{ 298 | $this->buffer .= Binary::writeLLong($v); 299 | } 300 | 301 | /** 302 | * Reads a 32-bit variable-length unsigned integer from the buffer and returns it. 303 | * 304 | * @phpstan-impure 305 | * @throws BinaryDataException 306 | */ 307 | public function getUnsignedVarInt() : int{ 308 | return Binary::readUnsignedVarInt($this->buffer, $this->offset); 309 | } 310 | 311 | /** 312 | * Writes a 32-bit variable-length unsigned integer to the end of the buffer. 313 | */ 314 | public function putUnsignedVarInt(int $v) : void{ 315 | $this->put(Binary::writeUnsignedVarInt($v)); 316 | } 317 | 318 | /** 319 | * Reads a 32-bit zigzag-encoded variable-length integer from the buffer and returns it. 320 | * 321 | * @phpstan-impure 322 | * @throws BinaryDataException 323 | */ 324 | public function getVarInt() : int{ 325 | return Binary::readVarInt($this->buffer, $this->offset); 326 | } 327 | 328 | /** 329 | * Writes a 32-bit zigzag-encoded variable-length integer to the end of the buffer. 330 | */ 331 | public function putVarInt(int $v) : void{ 332 | $this->put(Binary::writeVarInt($v)); 333 | } 334 | 335 | /** 336 | * Reads a 64-bit variable-length integer from the buffer and returns it. 337 | * 338 | * @phpstan-impure 339 | * @throws BinaryDataException 340 | */ 341 | public function getUnsignedVarLong() : int{ 342 | return Binary::readUnsignedVarLong($this->buffer, $this->offset); 343 | } 344 | 345 | /** 346 | * Writes a 64-bit variable-length integer to the end of the buffer. 347 | */ 348 | public function putUnsignedVarLong(int $v) : void{ 349 | $this->buffer .= Binary::writeUnsignedVarLong($v); 350 | } 351 | 352 | /** 353 | * Reads a 64-bit zigzag-encoded variable-length integer from the buffer and returns it. 354 | * 355 | * @phpstan-impure 356 | * @throws BinaryDataException 357 | */ 358 | public function getVarLong() : int{ 359 | return Binary::readVarLong($this->buffer, $this->offset); 360 | } 361 | 362 | /** 363 | * Writes a 64-bit zigzag-encoded variable-length integer to the end of the buffer. 364 | */ 365 | public function putVarLong(int $v) : void{ 366 | $this->buffer .= Binary::writeVarLong($v); 367 | } 368 | 369 | /** 370 | * Returns whether the offset has reached the end of the buffer. 371 | */ 372 | public function feof() : bool{ 373 | return !isset($this->buffer[$this->offset]); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/Limits.php: -------------------------------------------------------------------------------- 1 | expectException(BinaryDataException::class); 32 | Binary::readByte(""); 33 | } 34 | 35 | public function testReadSignedByteWithEmptyString() : void{ 36 | $this->expectException(BinaryDataException::class); 37 | Binary::readSignedByte(""); 38 | } 39 | 40 | /** 41 | * Passing too much data is OK; we just take the first N bytes as needed. 42 | */ 43 | public function testReadShortTooMuchData() : void{ 44 | $expected = 19132; 45 | $data = Binary::writeShort($expected) . "\x00\x00"; 46 | $number = Binary::readShort($data); 47 | self::assertSame($expected, $number); 48 | } 49 | 50 | public function testReadShortNotEnoughData() : void{ 51 | $this->expectException(BinaryDataException::class); 52 | Binary::readShort("\x01"); 53 | } 54 | } --------------------------------------------------------------------------------