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
-[](https://travis-ci.org/casperbiering/dotnet-binary-xml)
+[](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'],