diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a5a30901..ba7cde3b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -83,3 +83,45 @@ jobs: - run: composer update --prefer-dist --no-interaction --no-progress --ansi ${{ matrix.composer_option }} - run: vendor/bin/phpunit - run: vendor/bin/phpstan analyse --ansi --no-progress + tests-windows: + runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + include: + - description: 'Symfony 6.4 DEV' + php: '8.2' + symfony: '6.4.*@dev' + - description: 'Symfony 6.3' + php: '8.3' + symfony: '6.3.*' + - description: 'Symfony 6.3' + php: '8.2' + symfony: '6.3.*' + name: "[WINDOWS] PHP ${{ matrix.php }} tests (${{ matrix.description }})" + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Cache + uses: actions/cache@v3 + with: + path: ~/.composer/cache/files + key: composer-${{ matrix.php }}-${{ matrix.symfony }}-${{ matrix.composer_option }} + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - run: | + (Get-Content composer.json) -replace '("symfony/[^"]+": )"[^"]+"', '$1"${{ matrix.symfony }}"' | Out-File -encoding ASCII composer.json + if: matrix.symfony + - run: | + composer config minimum-stability dev + composer config prefer-stable true + if: matrix.beta + - name: remove cs-fixer for Symfony 6 + if: contains(matrix.symfony, '6.4.*@dev') + run: | + composer remove --dev friendsofphp/php-cs-fixer pedrotroller/php-cs-custom-fixer --no-update + - run: composer update --prefer-dist --no-interaction --no-progress --ansi ${{ matrix.composer_option }} + - run: vendor/bin/phpunit + - run: vendor/bin/phpstan analyse --ansi --no-progress diff --git a/src/Knp/Snappy/AbstractGenerator.php b/src/Knp/Snappy/AbstractGenerator.php index 1189e95a..2a93529b 100644 --- a/src/Knp/Snappy/AbstractGenerator.php +++ b/src/Knp/Snappy/AbstractGenerator.php @@ -21,6 +21,10 @@ abstract class AbstractGenerator implements GeneratorInterface, LoggerAwareInter { use LoggerAwareTrait; + protected const ALLOWED_PROTOCOLS = ['file']; + + protected const WINDOWS_LOCAL_FILENAME_REGEX = '/^[a-z]:(?:[\\\\\/]?(?:[\w\s!#()-]+|[\.]{1,2})+)*[\\\\\/]?/i'; + /** * @var array */ @@ -625,13 +629,8 @@ protected function executeCommand($command) */ protected function prepareOutput($filename, $overwrite) { - if (false === $parsedFilename = \parse_url($filename)) { - throw new InvalidArgumentException('The output filename is invalid.'); - } - - $scheme = isset($parsedFilename['scheme']) ? \mb_strtolower($parsedFilename['scheme']) : ''; - if ($scheme !== '' && $scheme !== 'file') { - throw new InvalidArgumentException(\sprintf('The output file scheme is not supported. Expected \'\' or \'file\' but got \'%s\'.', $scheme)); + if (!$this->isProtocolAllowed($filename)) { + throw new InvalidArgumentException(\sprintf('The output file scheme is not supported. Expected one of [\'%s\'].', \implode('\', \'', self::ALLOWED_PROTOCOLS))); } $directory = \dirname($filename); @@ -651,6 +650,34 @@ protected function prepareOutput($filename, $overwrite) } } + /** + * Verifies if the given filename has a supported protocol. + * + * @param string $filename + * + * @throws InvalidArgumentException + * + * @return bool + */ + protected function isProtocolAllowed($filename) + { + if (false === $parsedFilename = \parse_url($filename)) { + throw new InvalidArgumentException('The filename is not valid.'); + } + + $protocol = isset($parsedFilename['scheme']) ? \mb_strtolower($parsedFilename['scheme']) : 'file'; + + if ( + \PHP_OS_FAMILY === 'Windows' + && \strlen($protocol) === 1 + && \preg_match(self::WINDOWS_LOCAL_FILENAME_REGEX, $filename) + ) { + $protocol = 'file'; + } + + return \in_array($protocol, self::ALLOWED_PROTOCOLS, true); + } + /** * Wrapper for the "file_get_contents" function. *