From 6f28d57532b812809b8e18b2cda89b19366a8fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 23 Sep 2026 15:09:19 +0200 Subject: [PATCH 1/2] Skip tests writing to `nul` on Windows due to GHSA-9f67-6fw4-hpfp --- tests/AccessLogHandlerTest.php | 4 ++++ tests/AppMiddlewareTest.php | 4 ++++ tests/AppTest.php | 4 ++++ tests/Io/LogStreamHandlerTest.php | 7 ++++++- tests/integration/tests/AppInvokeIndexReturnsResponse.phpt | 2 ++ .../tests/AppStopsWithoutOutputWithNullRunner.phpt | 2 ++ 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/AccessLogHandlerTest.php b/tests/AccessLogHandlerTest.php index f3bfeb5..7d6a306 100644 --- a/tests/AccessLogHandlerTest.php +++ b/tests/AccessLogHandlerTest.php @@ -122,6 +122,10 @@ public function testInvokeWithPathToExistingFileWillAppendLogMessage(): void */ public function testInvokeWithDevNullWritesNothing(): void { + if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') === false) { + $this->markTestSkipped('Not supported on Windows due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp'); + } + $handler = new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'); $request = new ServerRequest('GET', 'http://localhost:8080/users', [], '', '1.1', ['REMOTE_ADDR' => '127.0.0.1']); diff --git a/tests/AppMiddlewareTest.php b/tests/AppMiddlewareTest.php index 0312554..a9c07df 100644 --- a/tests/AppMiddlewareTest.php +++ b/tests/AppMiddlewareTest.php @@ -647,6 +647,10 @@ public function testInvokeWithGlobalMiddlewareReturnsResponseWhenGlobalMiddlewar /** @param callable|class-string ...$middleware */ private function createAppWithoutLogger(...$middleware): App { + if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') === false) { + $this->markTestSkipped('Not supported on Windows due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp'); + } + return new App( new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'), new ErrorHandler(), diff --git a/tests/AppTest.php b/tests/AppTest.php index fee95df..a9875f6 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -1996,6 +1996,10 @@ public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhe private function createAppWithoutLogger(callable ...$middleware): App { + if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') === false) { + $this->markTestSkipped('Not supported on Windows due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp'); + } + return new App( new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'), new ErrorHandler(), diff --git a/tests/Io/LogStreamHandlerTest.php b/tests/Io/LogStreamHandlerTest.php index 8c773e7..5790f9e 100644 --- a/tests/Io/LogStreamHandlerTest.php +++ b/tests/Io/LogStreamHandlerTest.php @@ -91,8 +91,9 @@ public static function provideFilesystemPaths(): \Generator '\\\\.\\nul', false ]; - if (DIRECTORY_SEPARATOR === '\\') { + if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') !== false) { // valid path on Windows, but we don't want to write here on Unix + // not supported on newer Windows versions due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp yield [ __DIR__ . DIRECTORY_SEPARATOR . 'nul', true @@ -286,6 +287,10 @@ public function testLogWithPathToExistingFileWillAppendLogMessageWithCurrentDate */ public function testLogWithDevNullWritesNothing(): void { + if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') === false) { + $this->markTestSkipped('Not supported on Windows due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp'); + } + $logger = new LogStreamHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'); $logger->log('Hello'); diff --git a/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt b/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt index 4db264f..59aa987 100644 --- a/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt +++ b/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt @@ -1,5 +1,7 @@ --TEST-- Loading index file with NullRunner allows invoking the app +--SKIPIF-- + --INI-- # suppress legacy PHPUnit 7 warning for Xdebug 3 xdebug.default_enable= diff --git a/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt b/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt index 80bf4b7..99094af 100644 --- a/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt +++ b/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt @@ -1,5 +1,7 @@ --TEST-- Loading index file with NullRunner stops immediately without output +--SKIPIF-- + --INI-- # suppress legacy PHPUnit 7 warning for Xdebug 3 xdebug.default_enable= From a9446fa490d3db44aa9a22875a297c3daed86689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 23 Sep 2026 15:10:06 +0200 Subject: [PATCH 2/2] Support `nul` log path on Windows --- docs/api/app.md | 6 +++--- src/AccessLogHandler.php | 4 ++-- src/Io/LogStreamHandler.php | 6 +++--- tests/AccessLogHandlerTest.php | 6 +----- tests/AppMiddlewareTest.php | 6 +----- tests/AppTest.php | 6 +----- tests/Io/LogStreamHandlerTest.php | 14 +++++++++----- tests/integration/public/index.php | 2 +- .../tests/AppInvokeIndexReturnsResponse.phpt | 2 -- .../tests/AppStopsWithoutOutputWithNullRunner.phpt | 2 -- 10 files changed, 21 insertions(+), 33 deletions(-) diff --git a/docs/api/app.md b/docs/api/app.md index ca82526..554b78e 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -400,8 +400,8 @@ like this: // … ``` -Likewise, you can disable writing an access log by passing an absolute path to -`/dev/null` (Unix) or `nul` (Windows) like this: +Likewise, you can disable writing an access log by passing `/dev/null` (Unix) or +`nul` (Windows) like this: ```php title="public/index.php" DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul' + 'accesslog' => DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : 'nul', FrameworkX\AccessLogHandler::class => fn(string $accesslog) => new FrameworkX\AccessLogHandler($accesslog), ]); diff --git a/src/AccessLogHandler.php b/src/AccessLogHandler.php index 56ed811..80fd017 100644 --- a/src/AccessLogHandler.php +++ b/src/AccessLogHandler.php @@ -21,8 +21,8 @@ class AccessLogHandler private $hasHighResolution; /** - * @param ?string $path (optional) absolute log file path or will log to console output by default - * @throws \InvalidArgumentException if given `$path` is not an absolute file path + * @param ?string $path (optional) logs to console output by default, otherwise can be an absolute log file path, a `nul` path on Windows or a `php://` stream + * @throws \InvalidArgumentException if given `$path` is not an absolute file path, `nul` on Windows or `php://` stream * @throws \RuntimeException if given `$path` can not be opened in append mode */ public function __construct(?string $path = null) diff --git a/src/Io/LogStreamHandler.php b/src/Io/LogStreamHandler.php index 7d53b9a..d0d98d2 100644 --- a/src/Io/LogStreamHandler.php +++ b/src/Io/LogStreamHandler.php @@ -11,8 +11,8 @@ class LogStreamHandler private $stream; /** - * @param string $path absolute log file path - * @throws \InvalidArgumentException if given `$path` is not an absolute file path + * @param string $path absolute log file path, a `nul` path on Windows or a `php://` stream + * @throws \InvalidArgumentException if given `$path` is not an absolute file path, `nul` on Windows or `php://` stream * @throws \RuntimeException if given `$path` can not be opened in append mode */ public function __construct(string $path) @@ -90,6 +90,6 @@ public function log(string $message): void private function isAbsolutePath(string $path): bool { - return \DIRECTORY_SEPARATOR !== '\\' ? \substr($path, 0, 1) === '/' : (bool) \preg_match('#^[A-Z]:[/\\\\]#i', $path); + return \DIRECTORY_SEPARATOR !== '\\' ? \substr($path, 0, 1) === '/' : (bool) \preg_match('#^(?:[A-Z]:[/\\\\]|nul$)#i', $path); } } diff --git a/tests/AccessLogHandlerTest.php b/tests/AccessLogHandlerTest.php index 7d6a306..80c55c1 100644 --- a/tests/AccessLogHandlerTest.php +++ b/tests/AccessLogHandlerTest.php @@ -122,11 +122,7 @@ public function testInvokeWithPathToExistingFileWillAppendLogMessage(): void */ public function testInvokeWithDevNullWritesNothing(): void { - if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') === false) { - $this->markTestSkipped('Not supported on Windows due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp'); - } - - $handler = new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'); + $handler = new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : 'nul'); $request = new ServerRequest('GET', 'http://localhost:8080/users', [], '', '1.1', ['REMOTE_ADDR' => '127.0.0.1']); $response = new Response(200, [], "Hello\n"); diff --git a/tests/AppMiddlewareTest.php b/tests/AppMiddlewareTest.php index a9c07df..242b070 100644 --- a/tests/AppMiddlewareTest.php +++ b/tests/AppMiddlewareTest.php @@ -647,12 +647,8 @@ public function testInvokeWithGlobalMiddlewareReturnsResponseWhenGlobalMiddlewar /** @param callable|class-string ...$middleware */ private function createAppWithoutLogger(...$middleware): App { - if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') === false) { - $this->markTestSkipped('Not supported on Windows due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp'); - } - return new App( - new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'), + new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : 'nul'), new ErrorHandler(), ...$middleware ); diff --git a/tests/AppTest.php b/tests/AppTest.php index a9875f6..5a0bb66 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -1996,12 +1996,8 @@ public function testInvokeWithMatchingRouteReturnsInternalServerErrorResponseWhe private function createAppWithoutLogger(callable ...$middleware): App { - if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') === false) { - $this->markTestSkipped('Not supported on Windows due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp'); - } - return new App( - new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'), + new AccessLogHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : 'nul'), new ErrorHandler(), ...$middleware ); diff --git a/tests/Io/LogStreamHandlerTest.php b/tests/Io/LogStreamHandlerTest.php index 5790f9e..241e514 100644 --- a/tests/Io/LogStreamHandlerTest.php +++ b/tests/Io/LogStreamHandlerTest.php @@ -85,6 +85,14 @@ public static function provideFilesystemPaths(): \Generator ]; yield [ 'nul', + DIRECTORY_SEPARATOR === '\\' + ]; + yield [ + 'NUL', + DIRECTORY_SEPARATOR === '\\' + ]; + yield [ + 'nul.txt', false ]; yield [ @@ -287,11 +295,7 @@ public function testLogWithPathToExistingFileWillAppendLogMessageWithCurrentDate */ public function testLogWithDevNullWritesNothing(): void { - if (DIRECTORY_SEPARATOR === '\\' && @fopen(__DIR__ . '\\nul', 'a') === false) { - $this->markTestSkipped('Not supported on Windows due to https://github.com/php/php-src/security/advisories/GHSA-9f67-6fw4-hpfp'); - } - - $logger = new LogStreamHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul'); + $logger = new LogStreamHandler(DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : 'nul'); $logger->log('Hello'); } diff --git a/tests/integration/public/index.php b/tests/integration/public/index.php index 1fceb3a..f89f7b4 100644 --- a/tests/integration/public/index.php +++ b/tests/integration/public/index.php @@ -29,7 +29,7 @@ function asleep(float $s): PromiseInterface $container = new FrameworkX\Container([ FrameworkX\AccessLogHandler::class => function (?string $X_EXPERIMENTAL_RUNNER = null) { // log to /dev/null when running in experimental runner mode to avoid cluttering output - return new FrameworkX\AccessLogHandler($X_EXPERIMENTAL_RUNNER !== null ? (DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : __DIR__ . '\\nul') : null); + return new FrameworkX\AccessLogHandler($X_EXPERIMENTAL_RUNNER !== null ? (DIRECTORY_SEPARATOR !== '\\' ? '/dev/null' : 'nul') : null); } ]); diff --git a/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt b/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt index 59aa987..4db264f 100644 --- a/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt +++ b/tests/integration/tests/AppInvokeIndexReturnsResponse.phpt @@ -1,7 +1,5 @@ --TEST-- Loading index file with NullRunner allows invoking the app ---SKIPIF-- - --INI-- # suppress legacy PHPUnit 7 warning for Xdebug 3 xdebug.default_enable= diff --git a/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt b/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt index 99094af..80bf4b7 100644 --- a/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt +++ b/tests/integration/tests/AppStopsWithoutOutputWithNullRunner.phpt @@ -1,7 +1,5 @@ --TEST-- Loading index file with NullRunner stops immediately without output ---SKIPIF-- - --INI-- # suppress legacy PHPUnit 7 warning for Xdebug 3 xdebug.default_enable=