From 937114010dd749eb0617345d5c2d8513acec3909 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 15 Jul 2026 11:48:43 +0200 Subject: [PATCH 1/3] feat: Allow to stream input Signed-off-by: Carl Schwan --- lib/Service.php | 47 ++++++++++++++++++++++----------- phpstan-rules-lt-8.4.neon | 10 +++++++ phpstan.conditional.php | 20 ++++++++++++++ phpstan.neon | 1 + tests/Sabre/Xml/ServiceTest.php | 6 +++-- 5 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 phpstan-rules-lt-8.4.neon create mode 100644 phpstan.conditional.php diff --git a/lib/Service.php b/lib/Service.php index 747587e..16b213f 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -109,27 +109,42 @@ public function getWriter(): Writer */ public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null) { - if (!is_string($input)) { - // Unfortunately the XMLReader doesn't support streams. When it - // does, we can optimize this. + if (PHP_VERSION_ID >= 80400) { if (is_resource($input)) { - $input = (string) stream_get_contents($input); + $r = Reader::fromStream($input, null, $this->options); + } elseif (is_string($input)) { + if ('' === $input) { + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); + } + $r = Reader::fromString($input, null, $this->options); } else { - // Input is not a string and not a resource. - // Therefore, it has to be a closed resource. - // Effectively empty input has been passed in. - $input = ''; + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); } - } - // If input is empty, then it's safe to throw an exception - if ('' === $input) { - throw new ParseException('The input element to parse is empty. Do not attempt to parse'); - } + $r->elementMap = $this->elementMap; + $r->contextUri = $contextUri; + } else { + if (!is_string($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } + } + // If input is empty, then it's safe to throw an exception + if ('' === $input) { + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); + } - $r = $this->getReader(); - $r->contextUri = $contextUri; - $r->XML($input, null, $this->options); + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->XML($input, null, $this->options); + } $result = $r->parse(); $rootElementName = $result['name']; diff --git a/phpstan-rules-lt-8.4.neon b/phpstan-rules-lt-8.4.neon new file mode 100644 index 0000000..b4dba40 --- /dev/null +++ b/phpstan-rules-lt-8.4.neon @@ -0,0 +1,10 @@ +parameters: + ignoreErrors: + - + rawMessage: 'Call to an undefined static method Sabre\Xml\Reader::fromStream().' + path: lib/Service.php + count: 1 + - + rawMessage: 'Call to an undefined static method Sabre\Xml\Reader::fromString().' + path: lib/Service.php + count: 1 diff --git a/phpstan.conditional.php b/phpstan.conditional.php new file mode 100644 index 0000000..97ea005 --- /dev/null +++ b/phpstan.conditional.php @@ -0,0 +1,20 @@ + Date: Wed, 15 Jul 2026 12:54:43 +0200 Subject: [PATCH 2/3] fix: Handle null value returned by getClark New error detected by phpstan with PHP 8.5 Signed-off-by: Carl Schwan --- lib/Deserializer/functions.php | 4 +++- lib/Reader.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Deserializer/functions.php b/lib/Deserializer/functions.php index c0ecb4d..e17006e 100644 --- a/lib/Deserializer/functions.php +++ b/lib/Deserializer/functions.php @@ -86,7 +86,9 @@ function keyValue(Reader $reader, ?string $namespace = null): array $values[$reader->localName] = $reader->parseCurrentElement()['value']; } else { $clark = $reader->getClark(); - $values[$clark] = $reader->parseCurrentElement()['value']; + if (null !== $clark) { + $values[$clark] = $reader->parseCurrentElement()['value']; + } } } else { if (!$reader->read()) { diff --git a/lib/Reader.php b/lib/Reader.php index 2d4b5c6..a08c5a9 100644 --- a/lib/Reader.php +++ b/lib/Reader.php @@ -273,7 +273,9 @@ public function parseAttributes(): array } $name = $this->getClark(); - $attributes[$name] = $this->value; + if (null !== $name) { + $attributes[$name] = $this->value; + } } else { $attributes[$this->localName] = $this->value; } From 87c911003c3207b817e21feb6ea7e79ff2ae329c Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 15 Jul 2026 14:25:07 +0200 Subject: [PATCH 3/3] tests: Add tests for PHP 8.4 with empty stream Signed-off-by: Carl Schwan --- lib/LibXMLException.php | 2 +- lib/Service.php | 1 + phpstan-baseline.neon | 2 ++ tests/Sabre/Xml/ServiceTest.php | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/LibXMLException.php b/lib/LibXMLException.php index 0f760c8..1020d79 100644 --- a/lib/LibXMLException.php +++ b/lib/LibXMLException.php @@ -29,7 +29,7 @@ public function __construct(/** */ protected array $errors, int $code = 0, ?\Throwable $previousException = null) { - parent::__construct($this->errors[0]->message.' on line '.$this->errors[0]->line.', column '.$this->errors[0]->column, $code, $previousException); + parent::__construct(trim($this->errors[0]->message).' on line '.$this->errors[0]->line.', column '.$this->errors[0]->column, $code, $previousException); } /** diff --git a/lib/Service.php b/lib/Service.php index 16b213f..f498ad0 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -147,6 +147,7 @@ public function parse($input, ?string $contextUri = null, ?string &$rootElementN } $result = $r->parse(); + $rootElementName = $result['name']; return $result['value']; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ec0c143..ed7313c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4,3 +4,5 @@ parameters: message: "#^Call to function is_null\\(\\) with null will always evaluate to true\\.$#" path: lib/Serializer/functions.php count: 1 + - message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::.*\(\).#' + path: tests/Sabre/Xml/ServiceTest.php diff --git a/tests/Sabre/Xml/ServiceTest.php b/tests/Sabre/Xml/ServiceTest.php index 8aeba2f..3ddc274 100644 --- a/tests/Sabre/Xml/ServiceTest.php +++ b/tests/Sabre/Xml/ServiceTest.php @@ -52,6 +52,21 @@ public function testEmptyInputParse($input): void $util->parse($input, '/sabre.io/ns'); } + public function testEmptyStream(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('This test requires PHP 8.4.'); + } + + $this->expectException(LibXMLException::class); + $this->expectExceptionMessage('Document is empty on line 1, column 1'); + + $emptyResource = fopen('php://input', 'r'); + $this->assertNotFalse($emptyResource); + $util = new Service(); + $util->parse($emptyResource, '/sabre.io/ns'); + } + #[Depends('testGetReader')] public function testParse(): void {