From 369c1e68cdf48d416aac487966ce4a33a9a15fbf Mon Sep 17 00:00:00 2001 From: Casper Biering Date: Tue, 2 Jun 2026 00:06:36 +0200 Subject: [PATCH] Harden decoder and refresh CI --- .editorconfig | 12 ++ .github/workflows/ci.yaml | 14 +- README.md | 2 +- composer.json | 22 ++- src/BinaryXml/Decoder.php | 254 +++++++++++++++++--------------- src/BinaryXml/Encoder.php | 145 ++++++++++-------- tests/BinaryXml/DecoderTest.php | 13 ++ 7 files changed, 275 insertions(+), 187 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..45a00a5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53d0e43..08164d6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,10 @@ name: CI on: + push: + branches: [main] pull_request: null + workflow_dispatch: null jobs: run: @@ -9,11 +12,11 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest, windows-latest] - php-versions: ['7.3', '7.4', '8.0', '8.1'] + php-versions: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -23,7 +26,10 @@ jobs: tools: composer - name: Install Dependencies - run: composer install --prefer-dist + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Validate Composer Metadata + run: composer validate --strict --no-check-publish - name: Run PHPUnit - run: ./vendor/bin/phpunit + run: composer test diff --git a/README.md b/README.md index 69f8cda..981f2f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # dotnet-binary-xml -[![Build Status](https://travis-ci.org/casperbiering/dotnet-binary-xml.png?branch=master)](https://travis-ci.org/casperbiering/dotnet-binary-xml) +[![CI](https://github.com/casperbiering/dotnet-binary-xml/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/casperbiering/dotnet-binary-xml/actions/workflows/ci.yaml) **dotnet-binary-xml** is a PHP implementation of the [.NET Binary Format: XML Data Structure (MC-NBFX)](http://msdn.microsoft.com/en-us/library/cc219210.aspx). diff --git a/composer.json b/composer.json index c00a021..5089e8d 100644 --- a/composer.json +++ b/composer.json @@ -18,9 +18,29 @@ }, "autoload-dev": { "psr-4": { - "CasperBiering\\Dotnet\\Tests\\": "src/" + "CasperBiering\\Dotnet\\Tests\\": "tests/" } }, + "scripts": { + "test": "phpunit", + "lint": [ + "@php -l src/BinaryXml.php", + "@php -l src/BinaryXml/Encoder.php", + "@php -l src/BinaryXml/Decoder.php", + "@php -l src/BinaryXml/SoapEncoder.php", + "@php -l src/BinaryXml/SoapDecoder.php", + "@php -l src/BinaryXml/Constants.php", + "@php -l src/BinaryXml/SoapConstants.php", + "@php -l src/BinaryXml/DecodingException.php", + "@php -l src/BinaryXml/EncodingException.php", + "@php -l tests/BinaryXml/DecoderTest.php", + "@php -l tests/BinaryXml/EncoderTest.php", + "@php -l tests/BinaryXml/TwoWayTest.php", + "@php -l tests/BinaryXml/SoapDecoderTest.php", + "@php -l tests/BinaryXml/SoapEncoderTest.php", + "@php -l decode.php" + ] + }, "license": "MIT", "authors": [ { diff --git a/src/BinaryXml/Decoder.php b/src/BinaryXml/Decoder.php index 0df98fc..da8fe47 100644 --- a/src/BinaryXml/Decoder.php +++ b/src/BinaryXml/Decoder.php @@ -34,7 +34,8 @@ public function decode($content) } while ($pos < $contentLength) { - switch (ord($content[$pos])) { + $currentRecordType = $this->peekByte($content, $pos); + switch ($currentRecordType) { case Constants::RECORD_TYPE_END_ELEMENT: $pos += 1; @@ -51,17 +52,15 @@ public function decode($content) $pos += 1; // Element - $pos += 1; // expect it to be 0x40 + $this->readByte($content, $pos); // expect it to be 0x40 $element = $this->readString($content, $pos); - $pos += 1; // expect it to be 0x01 + $this->readByte($content, $pos); // expect it to be 0x01 // Record type - $recordType = ord($content[$pos]); - $pos += 1; + $recordType = $this->readByte($content, $pos); // Length - $length = ord($content[$pos]); - $pos += 1; + $length = $this->readByte($content, $pos); // Entries for ($entry = 0; $entry < $length; $entry++) { @@ -168,8 +167,7 @@ public function decode($content) case Constants::RECORD_TYPE_PREFIX_DICTIONARY_ATTRIBUTE_X: case Constants::RECORD_TYPE_PREFIX_DICTIONARY_ATTRIBUTE_Y: case Constants::RECORD_TYPE_PREFIX_DICTIONARY_ATTRIBUTE_Z: - $char = chr(85 + ord($content[$pos])); - $pos += 1; + $char = chr(85 + $this->readByte($content, $pos)); $name = $this->readDictionaryString($content, $pos); $value = $this->readTextRecord($content, $pos); @@ -202,8 +200,7 @@ public function decode($content) case Constants::RECORD_TYPE_PREFIX_ATTRIBUTE_X: case Constants::RECORD_TYPE_PREFIX_ATTRIBUTE_Y: case Constants::RECORD_TYPE_PREFIX_ATTRIBUTE_Z: - $char = chr(59 + ord($content[$pos])); - $pos += 1; + $char = chr(59 + $this->readByte($content, $pos)); $name = $this->readString($content, $pos); $value = $this->readTextRecord($content, $pos); @@ -262,8 +259,7 @@ public function decode($content) case Constants::RECORD_TYPE_PREFIX_DICTIONARY_ELEMENT_X: case Constants::RECORD_TYPE_PREFIX_DICTIONARY_ELEMENT_Y: case Constants::RECORD_TYPE_PREFIX_DICTIONARY_ELEMENT_Z: - $char = chr(29 + ord($content[$pos])); - $pos += 1; + $char = chr(29 + $this->readByte($content, $pos)); $name = $this->readDictionaryString($content, $pos); @@ -295,8 +291,7 @@ public function decode($content) case Constants::RECORD_TYPE_PREFIX_ELEMENT_X: case Constants::RECORD_TYPE_PREFIX_ELEMENT_Y: case Constants::RECORD_TYPE_PREFIX_ELEMENT_Z: - $char = chr(3 + ord($content[$pos])); - $pos += 1; + $char = chr(3 + $this->readByte($content, $pos)); $name = $this->readString($content, $pos); @@ -370,7 +365,7 @@ public function decode($content) $writer->fullEndElement(); break; default: - throw new DecodingException(sprintf('Unknown record type 0x%02X at position %d.', ord($content[$pos]), $pos)); + throw new DecodingException(sprintf('Unknown record type 0x%02X at position %d.', $currentRecordType, $pos)); break; } } @@ -384,17 +379,15 @@ protected function readMultiByteInt31(&$content, &$pos) $value = 0; $last = 0x80; for ($i = 0; $i < 4 && ($last & 0x80); $i++) { - $last = ord($content[$pos]); + $last = $this->readByte($content, $pos); $value += ($last & 0x7F) << ($i * 7); - $pos++; } if ($i == 4 && $last & 0x80) { - $last = ord($content[$pos]); + $last = $this->readByte($content, $pos); if (($last & 0x7) !== $last) { throw new DecodingException(sprintf('Invalid MultiByteInt31 at position %d.', $start)); } $value += ($last & 0x7) << 28; - $pos++; } return $value; @@ -411,16 +404,14 @@ protected function readString(&$content, &$pos) { $recordLength = $this->readMultiByteInt31($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return $record; } protected function readTextRecord(&$content, &$pos) { - $recordType = ord($content[$pos]); - $pos += 1; + $recordType = $this->readByte($content, $pos); return $this->readTextRecordInner($content, $pos, $recordType); } @@ -446,24 +437,15 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) break; case Constants::RECORD_TYPE_INT8_TEXT: case Constants::RECORD_TYPE_INT8_TEXT_WITH_END_ELEMENT: - $record = unpack('c*', $content[$pos]); - $pos += 1; - - return (string) $record[1]; + return (string) $this->readInt8($content, $pos); break; case Constants::RECORD_TYPE_INT16_TEXT: case Constants::RECORD_TYPE_INT16_TEXT_WITH_END_ELEMENT: - $record = unpack('s*', substr($content, $pos, 2)); - $pos += 2; - - return (string) $record[1]; + return (string) $this->readInt16LE($content, $pos); break; case Constants::RECORD_TYPE_INT32_TEXT: case Constants::RECORD_TYPE_INT32_TEXT_WITH_END_ELEMENT: - $record = unpack('l*', substr($content, $pos, 4)); - $pos += 4; - - return (string) $record[1]; + return (string) $this->readInt32LE($content, $pos); break; case Constants::RECORD_TYPE_INT64_TEXT: case Constants::RECORD_TYPE_INT64_TEXT_WITH_END_ELEMENT: @@ -472,22 +454,22 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) throw new DecodingException('Int64 requires GMP extension'); } - list(, $int64Hex) = unpack('H*', strrev(substr($content, $pos, 8))); - $pos += 8; + $value = $this->readUInt64LE($content, $pos); + if (gmp_cmp($value, gmp_pow(2, 63)) >= 0) { + $value = gmp_sub($value, gmp_pow(2, 64)); + } - return (string) gmp_strval(gmp_init($int64Hex, 16), 10); + return (string) gmp_strval($value, 10); break; case Constants::RECORD_TYPE_FLOAT_TEXT: case Constants::RECORD_TYPE_FLOAT_TEXT_WITH_END_ELEMENT: - $record = unpack('f*', substr($content, $pos, 4)); - $pos += 4; + $record = unpack('g', $this->readBytes($content, $pos, 4)); return (string) $record[1]; break; case Constants::RECORD_TYPE_DOUBLE_TEXT: case Constants::RECORD_TYPE_DOUBLE_TEXT_WITH_END_ELEMENT: - $record = unpack('d*', substr($content, $pos, 8)); - $pos += 8; + $record = unpack('e', $this->readBytes($content, $pos, 8)); return (string) $record[1]; break; @@ -498,20 +480,16 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) throw new DecodingException('Decimal requires GMP extension'); } - $pos += 2; // First 2 bytes reserved + $this->readBytes($content, $pos, 2); // First 2 bytes reserved - $scale = ord($content[$pos]); - $pos += 1; + $scale = $this->readByte($content, $pos); - $sign = ord($content[$pos]); - $pos += 1; + $sign = $this->readByte($content, $pos); - list(, $hi32Hex) = unpack('H*', strrev(substr($content, $pos, 4))); - $pos += 4; + list(, $hi32Hex) = unpack('H*', strrev($this->readBytes($content, $pos, 4))); $hi32 = gmp_init($hi32Hex, 16); - list(, $lo64Hex) = unpack('H*', strrev(substr($content, $pos, 8))); - $pos += 8; + list(, $lo64Hex) = unpack('H*', strrev($this->readBytes($content, $pos, 8))); $lo64 = gmp_init($lo64Hex, 16); $value = gmp_add(gmp_mul($hi32, gmp_pow(2, 64)), $lo64); @@ -526,7 +504,7 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) $record = substr($record, 0, strlen($record) - $scale).'.'.substr($record, $scale * -1); $record = trim($record, '0'); - if ($record[0] == '.') { + if ($record === '' || $record[0] == '.') { $record = '0'.$record; } } @@ -547,10 +525,9 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) $binary = ''; for ($i = 0; $i < 8; $i++) { - list(, $byteint) = unpack('C*', $content[$pos + $i]); + list(, $byteint) = unpack('C*', $this->readBytes($content, $pos, 1)); $binary = sprintf('%08b', $byteint).$binary; } - $pos += 8; $value = gmp_init(substr($binary, 2, 62), 2); @@ -570,11 +547,11 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) // Join different time elements if ($fraction !== '0000000') { - $record = "${date}T${time}.${fraction}"; + $record = "{$date}T{$time}.{$fraction}"; } elseif ($time !== '00:00:00') { - $record = "${date}T${time}"; + $record = "{$date}T{$time}"; } else { - $record = "${date}"; + $record = "{$date}"; } // Add timezone info @@ -589,68 +566,56 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) break; case Constants::RECORD_TYPE_CHARS8_TEXT: case Constants::RECORD_TYPE_CHARS8_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('C*', $content[$pos]); - $pos += 1; + $recordLength = $this->readByte($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return $record; break; case Constants::RECORD_TYPE_CHARS16_TEXT: case Constants::RECORD_TYPE_CHARS16_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('S*', substr($content, $pos, 2)); - $pos += 2; + $recordLength = $this->readUInt16LE($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return $record; break; case Constants::RECORD_TYPE_CHARS32_TEXT: case Constants::RECORD_TYPE_CHARS32_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('l*', substr($content, $pos, 4)); - $pos += 4; + $recordLength = $this->readUInt32LE($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return $record; break; case Constants::RECORD_TYPE_BYTES8_TEXT: case Constants::RECORD_TYPE_BYTES8_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('C*', $content[$pos]); - $pos += 1; + $recordLength = $this->readByte($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return base64_encode($record); break; case Constants::RECORD_TYPE_BYTES16_TEXT: case Constants::RECORD_TYPE_BYTES16_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('S*', substr($content, $pos, 2)); - $pos += 2; + $recordLength = $this->readUInt16LE($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return base64_encode($record); break; case Constants::RECORD_TYPE_BYTES32_TEXT: case Constants::RECORD_TYPE_BYTES32_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('l*', substr($content, $pos, 4)); - $pos += 4; + $recordLength = $this->readUInt32LE($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return base64_encode($record); break; case Constants::RECORD_TYPE_START_LIST_TEXT: $record = ''; - while (ord($content[$pos]) != Constants::RECORD_TYPE_END_LIST_TEXT) { + while ($this->peekByte($content, $pos) != Constants::RECORD_TYPE_END_LIST_TEXT) { if ($record !== '') { $record .= ' '; } @@ -674,20 +639,15 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) case Constants::RECORD_TYPE_UUID_TEXT: case Constants::RECORD_TYPE_UUID_TEXT_WITH_END_ELEMENT: - list(, $data1) = unpack('H*', strrev(substr($content, $pos, 4))); - $pos += 4; + list(, $data1) = unpack('H*', strrev($this->readBytes($content, $pos, 4))); - list(, $data2) = unpack('H*', strrev(substr($content, $pos, 2))); - $pos += 2; + list(, $data2) = unpack('H*', strrev($this->readBytes($content, $pos, 2))); - list(, $data3) = unpack('H*', strrev(substr($content, $pos, 2))); - $pos += 2; + list(, $data3) = unpack('H*', strrev($this->readBytes($content, $pos, 2))); - list(, $data4) = unpack('H*', substr($content, $pos, 2)); - $pos += 2; + list(, $data4) = unpack('H*', $this->readBytes($content, $pos, 2)); - list(, $data5) = unpack('H*', substr($content, $pos, 6)); - $pos += 6; + list(, $data5) = unpack('H*', $this->readBytes($content, $pos, 6)); $record = "{$data1}-{$data2}-{$data3}-{$data4}-{$data5}"; @@ -706,10 +666,9 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) $value = ''; for ($i = 0; $i < 8; $i++) { - list(, $byte) = unpack('C*', $content[$pos + $i]); + list(, $byte) = unpack('C*', $this->readBytes($content, $pos, 1)); $value = sprintf('%08b', $byte).$value; } - $pos += 8; $value = gmp_init($value, 2); @@ -749,7 +708,7 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) if ($secs > 0 || $fracs > 0) { $record .= $secs; if ($fracs > 0) { - $record .= '.'.$fracs; + $record .= '.'.str_pad((string) $fracs, 7, '0', STR_PAD_LEFT); } $record .= 'S'; } @@ -764,15 +723,11 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) throw new DecodingException('Uint64 requires GMP extension'); } - list(, $uint64Hex) = unpack('H*', strrev(substr($content, $pos, 8))); - $pos += 8; - - return (string) gmp_strval(gmp_init($uint64Hex, 16), 10); + return (string) gmp_strval($this->readUInt64LE($content, $pos), 10); break; case Constants::RECORD_TYPE_BOOL_TEXT: case Constants::RECORD_TYPE_BOOL_TEXT_WITH_END_ELEMENT: - $record = ord($content[$pos]); - $pos += 1; + $record = $this->readByte($content, $pos); switch ($record) { case 0: return 'false'; @@ -786,38 +741,31 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) break; case Constants::RECORD_TYPE_UNICODECHARS8_TEXT: case Constants::RECORD_TYPE_UNICODECHARS8_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('C*', $content[$pos]); - $pos += 1; + $recordLength = $this->readByte($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return mb_convert_encoding($record, 'UTF-8', 'UTF-16'); break; case Constants::RECORD_TYPE_UNICODECHARS16_TEXT: case Constants::RECORD_TYPE_UNICODECHARS16_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('S*', substr($content, $pos, 2)); - $pos += 2; + $recordLength = $this->readUInt16LE($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return mb_convert_encoding($record, 'UTF-8', 'UTF-16'); break; case Constants::RECORD_TYPE_UNICODECHARS32_TEXT: case Constants::RECORD_TYPE_UNICODECHARS32_TEXT_WITH_END_ELEMENT: - list(, $recordLength) = unpack('l*', substr($content, $pos, 4)); - $pos += 4; + $recordLength = $this->readUInt32LE($content, $pos); - $record = substr($content, $pos, $recordLength); - $pos += $recordLength; + $record = $this->readBytes($content, $pos, $recordLength); return mb_convert_encoding($record, 'UTF-8', 'UTF-16'); break; case Constants::RECORD_TYPE_QNAMEDICTIONARY_TEXT: case Constants::RECORD_TYPE_QNAMEDICTIONARY_TEXT_WITH_END_ELEMENT: - $prefix = chr(97 + ord($content[$pos])); - $pos += 1; + $prefix = chr(97 + $this->readByte($content, $pos)); $name = $this->readDictionaryString($content, $pos); @@ -829,6 +777,80 @@ protected function readTextRecordInner(&$content, &$pos, $recordType) } } + protected function peekByte(&$content, $pos) + { + $this->assertAvailable($content, $pos, 1); + + return ord($content[$pos]); + } + + protected function readByte(&$content, &$pos) + { + $byte = $this->peekByte($content, $pos); + $pos += 1; + + return $byte; + } + + protected function readBytes(&$content, &$pos, $length) + { + $this->assertAvailable($content, $pos, $length); + + $bytes = substr($content, $pos, $length); + $pos += $length; + + return $bytes; + } + + protected function readInt8(&$content, &$pos) + { + $value = $this->readByte($content, $pos); + + return $value >= 0x80 ? $value - 0x100 : $value; + } + + protected function readUInt16LE(&$content, &$pos) + { + $record = unpack('v', $this->readBytes($content, $pos, 2)); + + return $record[1]; + } + + protected function readInt16LE(&$content, &$pos) + { + $value = $this->readUInt16LE($content, $pos); + + return $value >= 0x8000 ? $value - 0x10000 : $value; + } + + protected function readUInt32LE(&$content, &$pos) + { + $record = unpack('V', $this->readBytes($content, $pos, 4)); + + return $record[1]; + } + + protected function readInt32LE(&$content, &$pos) + { + $value = $this->readUInt32LE($content, $pos); + + return $value >= 0x80000000 ? $value - 0x100000000 : $value; + } + + protected function readUInt64LE(&$content, &$pos) + { + list(, $hex) = unpack('H*', strrev($this->readBytes($content, $pos, 8))); + + return gmp_init($hex, 16); + } + + protected function assertAvailable(&$content, $pos, $length) + { + if ($length < 0 || $pos < 0 || strlen($content) - $pos < $length) { + throw new DecodingException(sprintf('Unexpected end of content at position %d.', $pos)); + } + } + protected function getDictionaryString($index) { if (is_string($this->options['dictionary'])) { diff --git a/src/BinaryXml/Encoder.php b/src/BinaryXml/Encoder.php index 738f91b..e176396 100644 --- a/src/BinaryXml/Encoder.php +++ b/src/BinaryXml/Encoder.php @@ -38,79 +38,94 @@ public function encode($xml) return ''; } + $previousUseInternalErrors = libxml_use_internal_errors(true); libxml_clear_errors(); $reader = new XMLReader(); - $reader->xml($xml); + if (!$reader->xml($xml)) { + $last_error = libxml_get_last_error(); + libxml_clear_errors(); + libxml_use_internal_errors($previousUseInternalErrors); - $binary = ''; - while (@$reader->read()) { - switch ($reader->nodeType) { - case XMLReader::NONE: - break; - - case XMLReader::ELEMENT: - $index = $this->getDictionaryIndex($reader->localName); - if ($reader->prefix && $index !== false) { - $binary .= chr(Constants::RECORD_TYPE_DICTIONARY_ELEMENT); - $this->writeString($binary, $reader->prefix); - $this->writeDictionaryString($binary, $index); - } elseif ($reader->prefix) { - $binary .= chr(Constants::RECORD_TYPE_ELEMENT); - $this->writeString($binary, $reader->prefix); - $this->writeString($binary, $reader->localName); - } elseif ($index !== false) { - $binary .= chr(Constants::RECORD_TYPE_SHORT_DICTIONARY_ELEMENT); - $this->writeDictionaryString($binary, $index); - } else { - $binary .= chr(Constants::RECORD_TYPE_SHORT_ELEMENT); - $this->writeString($binary, $reader->localName); - } - $emptyElement = $reader->isEmptyElement; - $this->writeAttributes($binary, $reader); - if ($emptyElement) { - $binary .= chr(Constants::RECORD_TYPE_END_ELEMENT); - } - break; + $message = $last_error === false ? 'Unknown XML parsing error' : rtrim($last_error->message); - case XMLReader::TEXT: - $this->writeTextRecord($binary, $reader->value); - break; + throw new EncodingException(sprintf('XML Parsing Error "%s".', $message)); + } - case XMLReader::COMMENT: - $binary .= chr(Constants::RECORD_TYPE_COMMENT); - $this->writeString($binary, $reader->value); - break; - - case XMLReader::WHITESPACE: - break; - - case XMLReader::SIGNIFICANT_WHITESPACE: - $this->writeTextRecord($binary, $reader->value); - break; - - case XMLReader::END_ELEMENT: - // TODO: Use *_WITH_END_ELEMENT if possible - $binary .= chr(Constants::RECORD_TYPE_END_ELEMENT); - break; - - case XMLReader::ATTRIBUTE: - throw new EncodingException('Invalid encoding state.'); - case XMLReader::DOC: - case XMLReader::DOC_TYPE: - case XMLReader::DOC_FRAGMENT: - case XMLReader::NOTATION: - case XMLReader::XML_DECLARATION: - case XMLReader::CDATA: - case XMLReader::ENTITY: - case XMLReader::ENTITY_REF: - case XMLReader::END_ENTITY: - case XMLReader::PI: - default: - throw new EncodingException(sprintf('Unsupported XML node type %d.', $reader->nodeType)); + $binary = ''; + try { + while ($reader->read()) { + switch ($reader->nodeType) { + case XMLReader::NONE: + break; + + case XMLReader::ELEMENT: + $index = $this->getDictionaryIndex($reader->localName); + if ($reader->prefix && $index !== false) { + $binary .= chr(Constants::RECORD_TYPE_DICTIONARY_ELEMENT); + $this->writeString($binary, $reader->prefix); + $this->writeDictionaryString($binary, $index); + } elseif ($reader->prefix) { + $binary .= chr(Constants::RECORD_TYPE_ELEMENT); + $this->writeString($binary, $reader->prefix); + $this->writeString($binary, $reader->localName); + } elseif ($index !== false) { + $binary .= chr(Constants::RECORD_TYPE_SHORT_DICTIONARY_ELEMENT); + $this->writeDictionaryString($binary, $index); + } else { + $binary .= chr(Constants::RECORD_TYPE_SHORT_ELEMENT); + $this->writeString($binary, $reader->localName); + } + $emptyElement = $reader->isEmptyElement; + $this->writeAttributes($binary, $reader); + if ($emptyElement) { + $binary .= chr(Constants::RECORD_TYPE_END_ELEMENT); + } + break; + + case XMLReader::TEXT: + $this->writeTextRecord($binary, $reader->value); + break; + + case XMLReader::COMMENT: + $binary .= chr(Constants::RECORD_TYPE_COMMENT); + $this->writeString($binary, $reader->value); + break; + + case XMLReader::WHITESPACE: + break; + + case XMLReader::SIGNIFICANT_WHITESPACE: + $this->writeTextRecord($binary, $reader->value); + break; + + case XMLReader::END_ELEMENT: + // TODO: Use *_WITH_END_ELEMENT if possible + $binary .= chr(Constants::RECORD_TYPE_END_ELEMENT); + break; + + case XMLReader::ATTRIBUTE: + throw new EncodingException('Invalid encoding state.'); + case XMLReader::DOC: + case XMLReader::DOC_TYPE: + case XMLReader::DOC_FRAGMENT: + case XMLReader::NOTATION: + case XMLReader::XML_DECLARATION: + case XMLReader::CDATA: + case XMLReader::ENTITY: + case XMLReader::ENTITY_REF: + case XMLReader::END_ENTITY: + case XMLReader::PI: + default: + throw new EncodingException(sprintf('Unsupported XML node type %d.', $reader->nodeType)); + } } + + $last_error = libxml_get_last_error(); + } finally { + libxml_clear_errors(); + libxml_use_internal_errors($previousUseInternalErrors); } - $last_error = libxml_get_last_error(); if ($last_error !== false) { throw new EncodingException(sprintf('XML Parsing Error "%s".', rtrim($last_error->message))); } diff --git a/tests/BinaryXml/DecoderTest.php b/tests/BinaryXml/DecoderTest.php index 1bd1937..713553e 100644 --- a/tests/BinaryXml/DecoderTest.php +++ b/tests/BinaryXml/DecoderTest.php @@ -61,6 +61,17 @@ public function testInvalidMultiByteInt31() $decoder->decode($binary); } + public function testTruncatedString() + { + $this->expectException(\CasperBiering\Dotnet\BinaryXml\DecodingException::class); + $this->expectExceptionMessage('Unexpected end of content at position 2.'); + + $binary = $this->convertToBinary('40 03 64 6F'); + $decoder = new Decoder(); + + $decoder->decode($binary); + } + public function testInvalidRecordType() { $this->expectException(\CasperBiering\Dotnet\BinaryXml\DecodingException::class); @@ -184,12 +195,14 @@ public function samples() ['42 80 80 80 01 01', ''], ['42 80 80 80 80 01 01', ''], ['40 03 64 6F 63 06 EC 01 8E 00 00 00 80 00 00 00 00 01', ''], + ['40 03 64 6F 63 8F FF FF FF FF FF FF FF FF', '-1'], ['42 9A 01 8F 00 00 00 00 00 01 00 00', '1099511627776'], ['40 03 64 6F 63 B2 FF FF FF FF FF FF FF FF 01', '18446744073709551615'], ['42 9A 01 B3 FE FF FF FF FF FF FF FF', '18446744073709551614'], ['40 03 64 6F 63 AC 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF 01', 'urn:uuid:33221100-5544-7766-8899-aabbccddeeff'], ['42 1A AD 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF', 'urn:uuid:33221100-5544-7766-8899-aabbccddeeff'], ['40 03 64 6F 63 AE 00 C4 F5 32 FF FF FF FF 01', '-PT5M44S'], + ['40 03 64 6F 63 AE 01 00 00 00 00 00 00 00 01', 'PT0.0000001S'], ['42 94 07 AF 00 B0 8E F0 1B 00 00 00', 'PT3H20M'], ['40 03 64 6F 63 B0 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 01', '03020100-0504-0706-0809-0a0b0c0d0e0f'], ['40 02 49 44 B1 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F', '03020100-0504-0706-0809-0a0b0c0d0e0f'],