├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── lib └── Kaitai │ └── Struct │ ├── Error │ ├── EndOfStreamError.php │ ├── KaitaiError.php │ ├── KaitaiStructError.php │ ├── NoTerminatorFoundError.php │ ├── NotSupportedPlatformError.php │ ├── ProcessError.php │ ├── RotateProcessError.php │ ├── UndecidedEndiannessError.php │ ├── ValidationExprError.php │ ├── ValidationFailedError.php │ ├── ValidationGreaterThanError.php │ ├── ValidationLessThanError.php │ ├── ValidationNotAnyOfError.php │ ├── ValidationNotEqualError.php │ ├── ValidationNotInEnumError.php │ └── ZlibProcessError.php │ ├── Stream.php │ └── Struct.php └── test ├── KaitaiTest └── Struct │ ├── StreamTest.php │ └── _files │ └── fixed_struct.bin ├── bootstrap.php └── phpunit.xml /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2025 Kaitai Project: MIT license 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kaitai Struct: runtime library for PHP 2 | 3 | [![Packagist](https://img.shields.io/packagist/v/kaitai-io/kaitai_struct_php_runtime)](https://packagist.org/packages/kaitai-io/kaitai_struct_php_runtime) 4 | [![Packagist downloads](https://img.shields.io/packagist/dm/kaitai-io/kaitai_struct_php_runtime)](https://packagist.org/packages/kaitai-io/kaitai_struct_php_runtime/stats#:~:text=Last%2030%20days) 5 | [![PHP version](https://img.shields.io/packagist/php-v/kaitai-io/kaitai_struct_php_runtime)](https://packagist.org/packages/kaitai-io/kaitai_struct_php_runtime#:~:text=php%3A) 6 | 7 | This library implements Kaitai Struct API for PHP. 8 | 9 | Kaitai Struct is a declarative language used for describe various binary 10 | data structures, laid out in files or in memory: i.e. binary file 11 | formats, network stream packet formats, etc. 12 | 13 | Further reading: 14 | 15 | * [About Kaitai Struct](https://kaitai.io/) 16 | * [About API implemented in this library](https://doc.kaitai.io/stream_api.html) 17 | * [PHP-specific notes](https://doc.kaitai.io/lang_php.html) 18 | 19 | ## Licensing 20 | 21 | Copyright 2015-2025 Kaitai Project: MIT license 22 | 23 | Permission is hereby granted, free of charge, to any person obtaining 24 | a copy of this software and associated documentation files (the 25 | "Software"), to deal in the Software without restriction, including 26 | without limitation the rights to use, copy, modify, merge, publish, 27 | distribute, sublicense, and/or sell copies of the Software, and to 28 | permit persons to whom the Software is furnished to do so, subject to 29 | the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be 32 | included in all copies or substantial portions of the Software. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 35 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 36 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 37 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 38 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 39 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kaitai-io/kaitai_struct_php_runtime", 3 | "description": "Kaitai Struct: runtime library for PHP", 4 | "license": "MIT", 5 | "type": "library", 6 | "homepage": "https://github.com/kaitai-io/kaitai_struct_php_runtime", 7 | "support": { 8 | "issues": "https://github.com/kaitai-io/kaitai_struct_php_runtime/issues", 9 | "chat": "https://gitter.im/kaitai_struct/Lobby" 10 | }, 11 | "require": { 12 | "php": "^7.1.1 || ^8.0", 13 | "ext-iconv": "*" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^6.0 || ^8.0" 17 | }, 18 | "suggest": { 19 | "ext-zlib": "for `process: zlib` support" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Kaitai\\": "lib/Kaitai" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/EndOfStreamError.php: -------------------------------------------------------------------------------- 1 | bytesReq = $bytesReq; 11 | $this->bytesAvail = $bytesAvail; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/KaitaiError.php: -------------------------------------------------------------------------------- 1 | srcPath = $srcPath; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/NoTerminatorFoundError.php: -------------------------------------------------------------------------------- 1 | terminator = $terminator; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/NotSupportedPlatformError.php: -------------------------------------------------------------------------------- 1 | actual = $actual; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/ValidationFailedError.php: -------------------------------------------------------------------------------- 1 | pos() . ': validation failed: ' . $msg, $srcPath); 15 | $this->io = $io; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/ValidationGreaterThanError.php: -------------------------------------------------------------------------------- 1 | max = $max; 13 | $this->actual = $actual; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/ValidationLessThanError.php: -------------------------------------------------------------------------------- 1 | min = $min; 13 | $this->actual = $actual; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/ValidationNotAnyOfError.php: -------------------------------------------------------------------------------- 1 | actual = $actual; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/ValidationNotEqualError.php: -------------------------------------------------------------------------------- 1 | expected = $expected; 13 | $this->actual = $actual; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/ValidationNotInEnumError.php: -------------------------------------------------------------------------------- 1 | actual = $actual; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Error/ZlibProcessError.php: -------------------------------------------------------------------------------- 1 | stream = fopen('php://memory', 'r+b'); 29 | fwrite($this->stream, $stream); 30 | } else { 31 | $this->stream = $stream; 32 | } 33 | fseek($this->stream, 0, SEEK_SET); 34 | 35 | $this->alignToByte(); 36 | } 37 | 38 | /************************************************************************** 39 | * Stream positioning 40 | **************************************************************************/ 41 | 42 | public function isEof(): bool { 43 | if ($this->bitsLeft > 0) { 44 | return false; 45 | } 46 | 47 | // Unfortunately, feof() documentation in PHP is very unclear and, 48 | // in fact, its semantics follows C++ semantics with "read at least once 49 | // past the EOF first" => "set EOF flag on stream" => "eof returns true". 50 | // So, we'll have to emulate the same "one byte lookup" pattern from C++. 51 | 52 | if (fgetc($this->stream) === false) { 53 | // reached EOF 54 | return true; 55 | } else { 56 | // restore stream position, 1 byte back 57 | if (fseek($this->stream, -1, SEEK_CUR) !== 0) { 58 | throw new KaitaiError("Unable to roll back after reading a byte in isEof"); 59 | } 60 | return false; 61 | } 62 | } 63 | 64 | /** 65 | * @TODO: if $pos (int) > PHP_INT_MAX it becomes float in PHP. 66 | */ 67 | public function seek(int $pos)/*: void */ { 68 | $size = $this->size(); 69 | if ($pos > $size) { 70 | throw new KaitaiError("The position ($pos) must be less than the size ($size) of the stream"); 71 | } 72 | $res = fseek($this->stream, $pos); 73 | if ($res !== 0) { 74 | throw new KaitaiError("Unable to set new position"); 75 | } 76 | } 77 | 78 | public function pos(): int { 79 | return ftell($this->stream); 80 | } 81 | 82 | public function size(): int { 83 | return fstat($this->stream)['size']; 84 | } 85 | 86 | /************************************************************************** 87 | * Integer numbers 88 | **************************************************************************/ 89 | 90 | /************************************************************************** 91 | * Signed 92 | */ 93 | 94 | /** 95 | * Read 1 byte, signed integer 96 | */ 97 | public function readS1(): int { 98 | return unpack("c", $this->readBytes(1))[1]; 99 | } 100 | 101 | // --- 102 | // Big-endian 103 | 104 | public function readS2be(): int { 105 | return self::decodeSignedInt($this->readU2be(), self::SIGN_MASK_16); 106 | } 107 | 108 | public function readS4be(): int { 109 | return self::decodeSignedInt($this->readU4be(), self::SIGN_MASK_32); 110 | } 111 | 112 | public function readS8be(): int { 113 | // PHP does not support unsigned ints - all integers are signed. So 114 | // readU8be() actually returns a *signed* 64-bit integer, which is 115 | // exactly what we want here. See 116 | // : 117 | // 118 | // > **Caution** Note that PHP internally stores integral values as 119 | // > signed. If you unpack a large unsigned long and it is of the same 120 | // > size as PHP internally stored values the result will be a negative 121 | // > number even though unsigned unpacking was specified. 122 | return $this->readU8be(); 123 | } 124 | 125 | // -- 126 | // Little-endian 127 | 128 | public function readS2le(): int { 129 | return self::decodeSignedInt($this->readU2le(), self::SIGN_MASK_16); 130 | } 131 | 132 | public function readS4le(): int { 133 | return self::decodeSignedInt($this->readU4le(), self::SIGN_MASK_32); 134 | } 135 | 136 | public function readS8le(): int { 137 | // See comment above in readS8be() 138 | return $this->readU8le(); 139 | } 140 | 141 | /************************************************************************** 142 | * Unsigned 143 | */ 144 | 145 | public function readU1(): int { 146 | return unpack("C", $this->readBytes(1))[1]; 147 | } 148 | 149 | // --- 150 | // Big-endian 151 | 152 | public function readU2be(): int { 153 | return unpack("n", $this->readBytes(2))[1]; 154 | } 155 | 156 | public function readU4be(): int { 157 | return unpack("N", $this->readBytes(4))[1]; 158 | } 159 | 160 | public function readU8be(): int { 161 | return unpack("J", $this->readBytes(8))[1]; 162 | } 163 | 164 | // --- 165 | // Little-endian 166 | 167 | public function readU2le(): int { 168 | return unpack("v", $this->readBytes(2))[1]; 169 | } 170 | 171 | public function readU4le(): int { 172 | return unpack("V", $this->readBytes(4))[1]; 173 | } 174 | 175 | public function readU8le(): int { 176 | return unpack("P", $this->readBytes(8))[1]; 177 | } 178 | 179 | /************************************************************************** 180 | * Floating point numbers 181 | **************************************************************************/ 182 | 183 | // --- 184 | // Big-endian 185 | 186 | /** 187 | * Single precision floating-point number 188 | */ 189 | public function readF4be(): float { 190 | return unpack("G", $this->readBytes(4))[1]; 191 | } 192 | 193 | /** 194 | * Double precision floating-point number. 195 | */ 196 | public function readF8be(): float { 197 | return unpack("E", $this->readBytes(8))[1]; 198 | } 199 | 200 | // --- 201 | // Little-endian 202 | 203 | /** 204 | * Single precision floating-point number. 205 | */ 206 | public function readF4le(): float { 207 | return unpack("g", $this->readBytes(4))[1]; 208 | } 209 | 210 | /** 211 | * Double precision floating-point number. 212 | */ 213 | public function readF8le(): float { 214 | return unpack("e", $this->readBytes(8))[1]; 215 | } 216 | 217 | /************************************************************************** 218 | * Unaligned bit values 219 | **************************************************************************/ 220 | 221 | public function alignToByte()/*: void */ { 222 | $this->bitsLeft = 0; 223 | $this->bits = 0; 224 | } 225 | 226 | public function readBitsIntBe(int $n): int { 227 | $res = 0; 228 | 229 | $bitsNeeded = $n - $this->bitsLeft; 230 | $this->bitsLeft = -$bitsNeeded & 7; // `-$bitsNeeded mod 8` 231 | 232 | if ($bitsNeeded > 0) { 233 | // 1 bit => 1 byte 234 | // 8 bits => 1 byte 235 | // 9 bits => 2 bytes 236 | $bytesNeeded = (($bitsNeeded - 1) >> 3) + 1; // `ceil($bitsNeeded / 8)` (NB: `x >> 3` is `floor(x / 8)`) 237 | $buf = $this->readBytes($bytesNeeded); 238 | for ($i = 0; $i < $bytesNeeded; $i++) { 239 | $res = $res << 8 | ord($buf[$i]); 240 | } 241 | 242 | $newBits = $res; 243 | $res = self::zeroFillRightShift($res, $this->bitsLeft) | $this->bits << $bitsNeeded; 244 | $this->bits = $newBits; // will be masked at the end of the function 245 | } else { 246 | $res = self::zeroFillRightShift($this->bits, -$bitsNeeded); // shift unneeded bits out 247 | } 248 | 249 | $mask = (1 << $this->bitsLeft) - 1; // `bitsLeft` is in range 0..7, so `(1 << 63)` does not have to be considered 250 | $this->bits &= $mask; 251 | 252 | return $res; 253 | } 254 | 255 | /** 256 | * Unused since Kaitai Struct Compiler v0.9+ - compatibility with older versions 257 | * 258 | * @deprecated use {@link Stream::readBitsIntBe()} instead 259 | */ 260 | public function readBitsInt(int $n): int { 261 | return $this->readBitsIntBe($n); 262 | } 263 | 264 | public function readBitsIntLe(int $n): int { 265 | $res = 0; 266 | $bitsNeeded = $n - $this->bitsLeft; 267 | 268 | if ($bitsNeeded > 0) { 269 | // 1 bit => 1 byte 270 | // 8 bits => 1 byte 271 | // 9 bits => 2 bytes 272 | $bytesNeeded = (($bitsNeeded - 1) >> 3) + 1; // `ceil($bitsNeeded / 8)` (NB: `x >> 3` is `floor(x / 8)`) 273 | $buf = $this->readBytes($bytesNeeded); 274 | for ($i = 0; $i < $bytesNeeded; $i++) { 275 | $res |= ord($buf[$i]) << ($i * 8); 276 | } 277 | 278 | $newBits = self::zeroFillRightShift($res, $bitsNeeded); 279 | $res = $res << $this->bitsLeft | $this->bits; 280 | $this->bits = $newBits; 281 | } else { 282 | $res = $this->bits; 283 | $this->bits = self::zeroFillRightShift($this->bits, $n); 284 | } 285 | 286 | $this->bitsLeft = -$bitsNeeded & 7; // `-$bitsNeeded mod 8` 287 | 288 | $mask = self::getMaskOnes($n); 289 | $res &= $mask; 290 | return $res; 291 | } 292 | 293 | private static function getMaskOnes(int $n): int { 294 | // 1. (1 << 63) === PHP_INT_MIN (and yes, it is negative, because PHP uses signed 64-bit ints on 64-bit system), 295 | // so (1 << 63) - 1 gets converted to float and loses precision (leading to incorrect result) 296 | // 2. (1 << 64) - 1 works fine, because (1 << 64) === 0 (it overflows) and -1 is exactly what we want 297 | // (`php -r 'var_dump(decbin(-1));'` => string(64) "111...11") 298 | $bit = 1 << $n; 299 | return $bit === PHP_INT_MIN ? ~$bit : $bit - 1; 300 | } 301 | 302 | 303 | /************************************************************************** 304 | * Byte arrays 305 | **************************************************************************/ 306 | 307 | public function readBytes(int $numberOfBytes): string { 308 | // It is legitimate to ask for 0 bytes in Kaitai Struct API, 309 | // but PHP's fread() considers this an error, so check and 310 | // handle this case before calling fread() 311 | if ($numberOfBytes == 0) { 312 | return ''; 313 | } 314 | $bytes = fread($this->stream, $numberOfBytes); 315 | $n = strlen($bytes); 316 | if ($n < $numberOfBytes) { 317 | throw new EndOfStreamError($numberOfBytes, $n); 318 | } 319 | return $bytes; 320 | } 321 | 322 | public function readBytesFull(): string { 323 | return stream_get_contents($this->stream); 324 | } 325 | 326 | public function readBytesTerm($term, bool $includeTerm, bool $consumeTerm, bool $eosError): string { 327 | if (is_int($term)) { 328 | $term = chr($term); 329 | } 330 | $r = ''; 331 | while (true) { 332 | $c = fgetc($this->stream); 333 | if ($c === false) { 334 | if ($eosError) { 335 | throw new NoTerminatorFoundError($term); 336 | } 337 | break; 338 | } 339 | if ($c === $term) { 340 | if ($includeTerm) { 341 | $r .= $c; 342 | } 343 | if (!$consumeTerm) { 344 | $this->seek($this->pos() - 1); 345 | } 346 | break; 347 | } 348 | $r .= $c; 349 | } 350 | return $r; 351 | } 352 | 353 | public function readBytesTermMulti(string $term, bool $includeTerm, bool $consumeTerm, bool $eosError): string { 354 | $unitSize = strlen($term); 355 | 356 | // PHP's fread() considers asking for 0 bytes an error, so check and 357 | // handle this case before calling fread() 358 | if ($unitSize === 0) { 359 | return ''; 360 | } 361 | 362 | $r = ''; 363 | while (true) { 364 | $c = fread($this->stream, $unitSize); 365 | if ($c === false) { 366 | $c = ''; 367 | } 368 | if (strlen($c) < $unitSize) { 369 | if ($eosError) { 370 | throw new NoTerminatorFoundError($term); 371 | } 372 | $r .= $c; 373 | break; 374 | } 375 | if ($c === $term) { 376 | if ($includeTerm) { 377 | $r .= $c; 378 | } 379 | if (!$consumeTerm) { 380 | $this->seek($this->pos() - $unitSize); 381 | } 382 | break; 383 | } 384 | $r .= $c; 385 | } 386 | return $r; 387 | } 388 | 389 | /** 390 | * @deprecated Unused since Kaitai Struct Compiler v0.9+ - compatibility with older versions 391 | */ 392 | public function ensureFixedContents(string $expectedBytes): string { 393 | $length = strlen($expectedBytes); 394 | $bytes = $this->readBytes($length); 395 | if ($bytes !== $expectedBytes) { 396 | // @TODO: print expected and actual bytes 397 | throw new \RuntimeException("Expected bytes are not equal to actual bytes"); 398 | } 399 | return $bytes; 400 | } 401 | 402 | public static function bytesStripRight(string $bytes, $padByte): string { 403 | if (is_int($padByte)) { 404 | $padByte = chr($padByte); 405 | } 406 | return rtrim($bytes, $padByte); 407 | } 408 | 409 | public static function bytesTerminate(string $bytes, $term, bool $includeTerm): string { 410 | if (is_int($term)) { 411 | $term = chr($term); 412 | } 413 | $newLen = strpos($bytes, $term); 414 | if ($newLen === false) { 415 | return $bytes; 416 | } else { 417 | if ($includeTerm) 418 | $newLen++; 419 | return substr($bytes, 0, $newLen); 420 | } 421 | } 422 | 423 | public static function bytesTerminateMulti(string $bytes, string $term, bool $includeTerm): string { 424 | $unitSize = strlen($term); 425 | $searchIndex = strpos($bytes, $term); 426 | while (true) { 427 | if ($searchIndex === false) { 428 | return $bytes; 429 | } 430 | $mod = $searchIndex % $unitSize; 431 | if ($mod === 0) { 432 | return substr($bytes, 0, $searchIndex + ($includeTerm ? $unitSize : 0)); 433 | } 434 | $searchIndex = strpos($bytes, $term, $searchIndex + ($unitSize - $mod)); 435 | } 436 | } 437 | 438 | public static function bytesToStr(string $bytes, string $encoding): string { 439 | return iconv($encoding, 'utf-8', $bytes); 440 | } 441 | 442 | public static function substring(string $string, int $from, int $to): string { 443 | return iconv_substr($string, $from, $to - $from); 444 | } 445 | 446 | /************************************************************************** 447 | * Byte array processing 448 | **************************************************************************/ 449 | 450 | /** 451 | * @param string $bytes 452 | * @param string|int $key 453 | * @return string 454 | */ 455 | public static function processXorOne(string $bytes, $key): string { 456 | if (is_string($key)) { 457 | $key = ord($key); 458 | } 459 | $xored = ''; 460 | for ($i = 0, $n = strlen($bytes); $i < $n; $i++) { 461 | $xored .= chr(ord($bytes[$i]) ^ $key); 462 | } 463 | return $xored; 464 | } 465 | 466 | public static function processXorMany(string $bytes, string $key): string { 467 | $keyLength = strlen($key); 468 | $xored = ''; 469 | for ($i = 0, $j = 0, $n = strlen($bytes); $i < $n; $i++, $j = ($j + 1) % $keyLength) { 470 | $xored .= chr(ord($bytes[$i]) ^ ord($key[$j])); 471 | } 472 | return $xored; 473 | } 474 | 475 | public static function processRotateLeft(string $bytes, int $amount, int $groupSize): string { 476 | if ($groupSize !== 1) { 477 | throw new RotateProcessError("Unable to rotate group of $groupSize bytes yet"); 478 | } 479 | $rotated = ''; 480 | for ($i = 0, $n = strlen($bytes); $i < $n; $i++) { 481 | $byte = ord($bytes[$i]); 482 | $rotated .= chr(($byte << $amount) | ($byte >> (8 - $amount))); 483 | } 484 | return $rotated; 485 | } 486 | 487 | public static function processZlib(string $bytes): string { 488 | $uncompressed = @gzuncompress($bytes); 489 | if (false === $uncompressed) { 490 | $error = error_get_last(); 491 | error_clear_last(); 492 | throw new ZlibProcessError($error['message']); 493 | } 494 | return $uncompressed; 495 | } 496 | 497 | /************************************************************************** 498 | * Misc runtime 499 | **************************************************************************/ 500 | 501 | /** 502 | * Performs modulo operation between two integers: dividend `a` 503 | * and divisor `b`. Divisor `b` is expected to be positive. The 504 | * result is always 0 <= x <= b - 1. 505 | */ 506 | public static function mod(int $a, int $b): int { 507 | return $a - (int)floor($a / $b) * $b; 508 | } 509 | 510 | public static function byteArrayMin(string $b): int { 511 | $min = PHP_INT_MAX; 512 | for ($i = 0, $n = strlen($b); $i < $n; $i++) { 513 | $value = ord($b[$i]); 514 | if ($value < $min) 515 | $min = $value; 516 | } 517 | return $min; 518 | } 519 | 520 | public static function byteArrayMax(string $b): int { 521 | $max = 0; 522 | for ($i = 0, $n = strlen($b); $i < $n; $i++) { 523 | $value = ord($b[$i]); 524 | if ($value > $max) 525 | $max = $value; 526 | } 527 | return $max; 528 | } 529 | 530 | /************************************************************************** 531 | * Internal 532 | **************************************************************************/ 533 | 534 | private static function decodeSignedInt(int $x, int $mask): int { 535 | // See https://graphics.stanford.edu/~seander/bithacks.html#VariableSignExtend 536 | return ($x ^ $mask) - $mask; 537 | } 538 | 539 | // From https://stackoverflow.com/a/14428473, modified 540 | private static function zeroFillRightShift(int $a, int $b): int { 541 | $res = $a >> $b; 542 | if ($a >= 0 || $b === 0) return $res; 543 | return $res & (PHP_INT_MAX >> ($b - 1)); 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /lib/Kaitai/Struct/Struct.php: -------------------------------------------------------------------------------- 1 | _io = $io; 11 | $this->_parent = $parent; 12 | $this->_root = $root; 13 | } 14 | 15 | public static function fromFile($filePath): Struct { 16 | return new static( 17 | new Stream( 18 | is_string($filePath) ? fopen($filePath, 'rb') : $filePath 19 | ) 20 | ); 21 | } 22 | 23 | public function __get($name) { 24 | if (method_exists($this, $name)) { 25 | return $this->$name(); 26 | } 27 | throw new \RuntimeException("Cannot access the property '" . get_class($this) . '::' . $name . "'"); 28 | } 29 | 30 | public function _parent(): ?Struct { 31 | return $this->_parent; 32 | } 33 | 34 | public function _root(): ?Struct { 35 | return $this->_root; 36 | } 37 | 38 | public function _io(): Stream { 39 | return $this->_io; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/KaitaiTest/Struct/StreamTest.php: -------------------------------------------------------------------------------- 1 | checkStreamPositioning( 15 | fopen(__DIR__ .'/_files/fixed_struct.bin', 'rb'), 16 | 148 17 | ); 18 | } 19 | 20 | public function testStreamPositioning_MemoryHandle() { 21 | $handle = $this->memoryHandle(); 22 | $s = 'abc'; 23 | $n = 200; 24 | fwrite($handle, str_repeat($s, $n)); 25 | $fileSize = strlen($s) * $n; 26 | $this->checkStreamPositioning($handle, $fileSize); 27 | } 28 | 29 | public function testStreamPositioning_String() { 30 | $s = str_repeat('abc', 200); 31 | $this->checkStreamPositioning($s, strlen($s)); 32 | } 33 | 34 | public function testS1() { 35 | $bytes = "\x80\xff\x00\x7f\xfa\x0f\xad\xe5\x22\x11"; 36 | $stream = new Stream($bytes); 37 | 38 | $this->assertSame(-128, $stream->readS1()); 39 | 40 | $stream->seek(1); 41 | $this->assertSame(-1, $stream->readS1()); 42 | 43 | $stream->seek(2); 44 | $this->assertSame(0, $stream->readS1()); 45 | 46 | $stream->seek(3); 47 | $this->assertSame(127, $stream->readS1()); 48 | 49 | $stream->seek(4); 50 | $this->assertSame(-6, $stream->readS1()); 51 | 52 | $stream->seek(5); 53 | $this->assertSame(15, $stream->readS1()); 54 | 55 | $stream->seek(6); 56 | $this->assertSame(-83, $stream->readS1()); 57 | 58 | $stream->seek(7); 59 | $this->assertSame(-27, $stream->readS1()); 60 | 61 | $stream->seek(8); 62 | $this->assertSame(34, $stream->readS1()); 63 | 64 | $stream->seek(9); 65 | $this->assertSame(17, $stream->readS1()); 66 | } 67 | 68 | public function testS2be() { 69 | $bytes = "\x80\x00" 70 | . "\xff\xff" 71 | . "\x00\x00" 72 | . "\x7f\xff"; 73 | $stream = new Stream($bytes); 74 | 75 | $this->assertSame(-32768, $stream->readS2be()); 76 | 77 | $stream->seek(2); 78 | $this->assertSame(-1, $stream->readS2be()); 79 | 80 | $stream->seek(4); 81 | $this->assertSame(0, $stream->readS2be()); 82 | 83 | $stream->seek(6); 84 | $this->assertSame(32767, $stream->readS2be()); 85 | } 86 | 87 | public function testS4be() { 88 | $bytes = "\x80\x00\x00\x00" 89 | . "\xff\xff\xff\xff" 90 | . "\x00\x00\x00\x00" 91 | . "\x7f\xff\xff\xff"; 92 | 93 | $stream = new Stream($bytes); 94 | 95 | $this->assertSame(-2147483648, $stream->readS4be()); 96 | 97 | $stream->seek(4); 98 | $this->assertSame(-1, $stream->readS4be()); 99 | 100 | $stream->seek(8); 101 | $this->assertSame(0, $stream->readS4be()); 102 | 103 | $stream->seek(12); 104 | $this->assertSame(2147483647, $stream->readS4be()); 105 | } 106 | 107 | public function testS8be() { 108 | $bytes = "\x80\x00\x00\x00\x00\x00\x00\x00" 109 | . "\xff\xff\xff\xff\xff\xff\xff\xff" 110 | . "\x00\x00\x00\x00\x00\x00\x00\x00" 111 | . "\x7f\xff\xff\xff\xff\xff\xff\xff"; 112 | $stream = new Stream($bytes); 113 | 114 | $this->assertSame(-9223372036854775807 - 1, $stream->readS8be()); 115 | 116 | $stream->seek(8); 117 | $this->assertSame(-1, $stream->readS8be()); 118 | 119 | $stream->seek(16); 120 | $this->assertSame(0, $stream->readS8be()); 121 | 122 | $stream->seek(24); 123 | $this->assertSame(9223372036854775807, $stream->readS8be()); 124 | } 125 | 126 | public function testS2le() { 127 | $bytes = "\x00\x80" 128 | . "\xff\xff" 129 | . "\x00\x00" 130 | . "\xff\x7f"; 131 | $stream = new Stream($bytes); 132 | 133 | $this->assertSame(-32768, $stream->readS2le()); 134 | 135 | $stream->seek(2); 136 | $this->assertSame(-1, $stream->readS2le()); 137 | 138 | $stream->seek(4); 139 | $this->assertSame(0, $stream->readS2le()); 140 | 141 | $stream->seek(6); 142 | $this->assertSame(32767, $stream->readS2le()); 143 | } 144 | 145 | public function testS4le() { 146 | $bytes = "\x00\x00\x00\x80" 147 | . "\xff\xff\xff\xff" 148 | . "\x00\x00\x00\x00" 149 | . "\xff\xff\xff\x7f"; 150 | $stream = new Stream($bytes); 151 | 152 | $this->assertSame(-2147483648, $stream->readS4le()); 153 | 154 | $stream->seek(4); 155 | $this->assertSame(-1, $stream->readS4le()); 156 | 157 | $stream->seek(8); 158 | $this->assertSame(0, $stream->readS4le()); 159 | 160 | $stream->seek(12); 161 | $this->assertSame(2147483647, $stream->readS4le()); 162 | } 163 | 164 | public function testS8le() { 165 | $bytes = "\x00\x00\x00\x00\x00\x00\x00\x80" 166 | . "\xff\xff\xff\xff\xff\xff\xff\xff" 167 | . "\x00\x00\x00\x00\x00\x00\x00\x00" 168 | . "\xff\xff\xff\xff\xff\xff\xff\x7f"; 169 | $stream = new Stream($bytes); 170 | 171 | $this->assertSame(-9223372036854775807 - 1, $stream->readS8le()); 172 | 173 | $stream->seek(8); 174 | $this->assertSame(-1, $stream->readS8le()); 175 | 176 | $stream->seek(16); 177 | $this->assertSame(0, $stream->readS8le()); 178 | 179 | $stream->seek(24); 180 | $this->assertSame(9223372036854775807, $stream->readS8le()); 181 | } 182 | 183 | public function testU1() { 184 | $bytes = "\x80\xff\x00\x7f\xfa\x0f\xad\xe5\x22\x11"; 185 | $stream = new Stream($bytes); 186 | $this->assertSame(128, $stream->readU1()); 187 | 188 | $stream->seek(1); 189 | $this->assertSame(255, $stream->readU1()); 190 | 191 | $stream->seek(2); 192 | $this->assertSame(0, $stream->readU1()); 193 | 194 | $stream->seek(3); 195 | $this->assertSame(127, $stream->readU1()); 196 | 197 | $stream->seek(4); 198 | $this->assertSame(250, $stream->readU1()); 199 | 200 | $stream->seek(5); 201 | $this->assertSame(15, $stream->readU1()); 202 | 203 | $stream->seek(6); 204 | $this->assertSame(173, $stream->readU1()); 205 | 206 | $stream->seek(7); 207 | $this->assertSame(229, $stream->readU1()); 208 | 209 | $stream->seek(8); 210 | $this->assertSame(34, $stream->readU1()); 211 | 212 | $stream->seek(9); 213 | $this->assertSame(17, $stream->readU1()); 214 | } 215 | 216 | public function dataForU2_LeBe() { 217 | return [ 218 | [ 219 | "\x00\x00" 220 | . "\x31\x12" 221 | . "\xff\xff", 222 | 'readU2le' 223 | ], 224 | [ 225 | "\x00\x00" 226 | . "\x12\x31" 227 | . "\xff\xff", 228 | 'readU2be' 229 | ], 230 | ]; 231 | } 232 | 233 | /** 234 | * @dataProvider dataForU2_LeBe 235 | */ 236 | public function testU2_LeBe(string $bytes, string $fn) { 237 | $stream = new Stream($bytes); 238 | $read = [$stream, $fn]; 239 | $this->assertSame(0, $read()); 240 | 241 | $stream->seek(2); 242 | $this->assertSame(4657, $read()); 243 | 244 | $stream->seek(4); 245 | $this->assertSame(65535, $read()); 246 | } 247 | 248 | public function dataForU4_LeBe() { 249 | return [ 250 | [ 251 | "\x00\x00\x00\x00" 252 | . "\x00\x12\x00\x0f" 253 | . "\xff\xff\xff\xff", 254 | 'readU4le' 255 | ], 256 | [ 257 | "\x00\x00\x00\x00" 258 | . "\x0f\x00\x12\x00" 259 | . "\xff\xff\xff\xff", 260 | 'readU4be' 261 | ], 262 | ]; 263 | } 264 | 265 | /** 266 | * @dataProvider dataForU4_LeBe 267 | */ 268 | public function testU4_LeBe(string $bytes, string $fn) { 269 | $stream = new Stream($bytes); 270 | $read = [$stream, $fn]; 271 | $this->assertSame(0, $read()); 272 | 273 | $stream->seek(4); 274 | $this->assertSame(251662848, $read()); 275 | 276 | $stream->seek(8); 277 | $this->assertSame(4294967295, $read()); 278 | } 279 | 280 | public function dataForU8_LeBe() { 281 | return [ 282 | [ 283 | "\x00\x00\x00\x00\x00\x00\x00\x00" 284 | . "\x00\x00\x00\x12\x00\xf0\x00\x00" 285 | . "\xff\xff\xff\xff\xff\xff\xff\xff", // 2^64 - 1 286 | 'readU8le' 287 | ], 288 | [ 289 | "\x00\x00\x00\x00\x00\x00\x00\x00" 290 | . "\x00\x00\xf0\x00\x12\x00\x00\x00" 291 | . "\xff\xff\xff\xff\xff\xff\xff\xff", // 2^64 - 1 292 | 'readU8be' 293 | ], 294 | ]; 295 | } 296 | 297 | /** 298 | * @dataProvider dataForU8_LeBe 299 | */ 300 | public function testU8_LeBe(string $bytes, string $fn) { 301 | $stream = new Stream($bytes); 302 | $read = [$stream, $fn]; 303 | 304 | $this->assertSame(0, $read()); 305 | 306 | $stream->seek(8); 307 | $this->assertSame(263883092656128, $read()); 308 | 309 | $stream->seek(16); 310 | // PHP does not support the unsigned integers, so to represent the values > 2^63-1 we 311 | // need to use signed integers, which have the same internal representation as unsigned. 312 | // In this case it is 2^64 - 1 313 | $this->assertSame(-1, $read()); 314 | } 315 | 316 | 317 | public function testReadF4be() { 318 | $bytes = "\xc0\x49\x0f\xdb"; 319 | // 1100 0000 0100 1001 0000 1111 1101 1011 320 | $stream = new Stream($bytes); 321 | $this->assertEquals(-3.141592653589793, $stream->readF4be(), '', self::SINGLE_EPS); 322 | // @TODO: test NAN, -INF, INF, -0.0, 0.0 323 | /* 324 | NaN 0x7FC00000. 325 | INF 0x7F800000. 326 | -INF 0xFF800000. 327 | */ 328 | } 329 | 330 | public function testReadF8be() { 331 | $this->markTestIncomplete(); 332 | // @TODO: test NAN, -INF, INF, -0.0, 0.0 333 | } 334 | 335 | public function testReadF4le() { 336 | $bytes = "\xdb\x0f\x49\xc0"; 337 | // 1101 1011 0000 1111 0100 1001 1100 0000 338 | $stream = new Stream($bytes); 339 | $this->assertEquals(-3.141592653589793, $stream->readF4le(), '', self::SINGLE_EPS); 340 | // @TODO: test NAN, -INF, INF, -0.0, 0.0 341 | } 342 | 343 | public function testReadF8le() { 344 | $this->markTestIncomplete(); 345 | // @TODO: test NAN, -INF, INF, -0.0, 0.0 346 | } 347 | 348 | public function testReadBytes_Consistently() { 349 | $bytes = "\x03\xef\xa4\xb9"; 350 | $stream = new Stream($bytes); 351 | $this->assertSame("\x03\xef", $stream->readBytes(2)); 352 | $this->assertSame("\xa4", $stream->readBytes(1)); 353 | $this->assertSame("\xb9", $stream->readBytes(1)); 354 | } 355 | 356 | public function testReadBytes_Seek() { 357 | $bytes = "\x03\xef\xa4\xb9"; 358 | $stream = new Stream($bytes); 359 | 360 | $stream->seek(1); 361 | $this->assertSame("\xef\xa4", $stream->readBytes(2)); 362 | 363 | $stream->seek(0); 364 | $this->assertSame("\x03\xef", $stream->readBytes(2)); 365 | 366 | $stream->seek(3); 367 | $this->assertSame("\xb9", $stream->readBytes(1)); 368 | } 369 | 370 | public function testReadBytesFull() { 371 | $bytes = "\x03\xef\xa4\xb9"; 372 | $stream = new Stream($bytes); 373 | 374 | $this->assertSame($bytes, $stream->readBytesFull()); 375 | $this->assertSame('', $stream->readBytesFull()); 376 | 377 | $stream->seek(0); 378 | $this->assertSame($bytes, $stream->readBytesFull()); 379 | $this->assertSame('', $stream->readBytesFull()); 380 | 381 | $stream->seek(2); 382 | $this->assertSame("\xa4\xb9", $stream->readBytesFull()); 383 | $this->assertSame('', $stream->readBytesFull()); 384 | } 385 | 386 | public function testEnsureFixedContents() { 387 | $bytes = "\x3c\x3f\x70\x68\x70"; // "assertSame( 390 | $bytes, 391 | $stream->ensureFixedContents($bytes) 392 | ); 393 | try { 394 | $stream->ensureFixedContents($bytes); 395 | $this->fail(); 396 | } catch (EndOfStreamError $e) { 397 | $this->assertSame('Requested ' . strlen($bytes) . ' bytes, but only 0 bytes available', $e->getMessage()); 398 | } 399 | } 400 | 401 | public function testProcessXorOne() { 402 | $stream = $this->stream(); 403 | 404 | $bytes = "\xab\x48\xf1\x04"; 405 | 406 | $xored = $stream::processXorOne($bytes, "\x3f"); // 63 int 407 | $this->assertSame("\x94\x77\xce\x3b", $xored); 408 | 409 | $xored = $stream::processXorOne($bytes, 63); 410 | $this->assertSame("\x94\x77\xce\x3b", $xored); 411 | } 412 | 413 | public function testProcessXorMany() { 414 | $stream = $this->stream(); 415 | $bytes = "\xab\x48\xf1\x04"; 416 | $key = "\x3f\x2d\xa5"; 417 | $xored = $stream::processXorMany($bytes, $key); 418 | $this->assertSame("\x94\x65\x54\x3b", $xored); 419 | } 420 | 421 | public function testProcessRotateLeft() { 422 | $stream = $this->stream(); 423 | $bytes = "\x17\x22\xc9\x04\x06\x13"; 424 | $rotated = $stream::processRotateLeft($bytes, 3, 1); 425 | $this->assertSame("\xb8\x11\x4e\x20\x30\x98", $rotated); 426 | } 427 | 428 | public function testProcessZlib() { 429 | $stream = $this->stream(); 430 | $string = "Compress me"; 431 | $compressed = gzcompress($string); 432 | $uncompressed = $stream::processZlib($compressed); 433 | $this->assertSame($string, $uncompressed); 434 | } 435 | 436 | private function memoryHandle() { 437 | return fopen("php://memory", "r+b"); 438 | } 439 | 440 | private function checkStreamPositioning($stream, $fileSize) { 441 | $stream = new Stream($stream); 442 | 443 | $this->assertSame($fileSize, $stream->size()); 444 | $this->assertSame(0, $stream->pos()); 445 | $this->assertFalse($stream->isEof()); 446 | 447 | $pos = 123; 448 | $this->assertNull($stream->seek($pos)); 449 | $this->assertSame($pos, $stream->pos()); 450 | $this->assertFalse($stream->isEof()); 451 | 452 | $this->assertSeekCallFailsForPos($stream, $fileSize + 3); 453 | 454 | $pos = $fileSize; 455 | $this->assertNull($stream->seek($pos)); 456 | $this->assertTrue($stream->isEof()); 457 | $this->assertSame($pos, $stream->pos()); 458 | } 459 | 460 | private function assertSeekCallFailsForPos(Stream $stream, $pos) { 461 | try { 462 | $this->assertNull($stream->seek($pos)); 463 | $this->fail(); 464 | } catch (KaitaiError $e) { 465 | $this->assertRegExp("~The position \\($pos\\) must be less than the size \\(\\d+\\) of the stream~s", $e->getMessage()); 466 | } 467 | } 468 | 469 | private function stream() { 470 | $handle = $this->memoryHandle(); 471 | return new Stream($handle); 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /test/KaitaiTest/Struct/_files/fixed_struct.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaitai-io/kaitai_struct_php_runtime/42fe5df15d906619ed89c8f76df99f13cb7c89ef/test/KaitaiTest/Struct/_files/fixed_struct.bin -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | KaitaiTest 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------