From 43a5121123636d803fe29c98a47e6ba27d674d8f Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Tue, 14 Nov 2023 20:40:20 -0600 Subject: [PATCH] Updated with latest updates from upstream, added tests --- .gitignore | 3 + .php-cs-fixer.php | 49 + composer.json | 9 +- composer.lock | 3193 ++++++++++++++++++- lib/Filesystem.php | 127 +- lib/Filesystem/FileNotFoundException.php | 3 +- lib/Filesystem/IOException.php | 3 +- lib/Filesystem/InvalidArgumentException.php | 7 +- lib/Filesystem/RuntimeException.php | 13 + lib/Path.php | 64 +- test | 38 + tests/bootstrap.php | 18 + tests/cases/TestExceptions.php | 41 + tests/cases/TestFilesystem.php | 1731 ++++++++++ tests/cases/TestPath.php | 1010 ++++++ tests/lib/FilesystemTestCase.php | 146 + tests/lib/MockStream.php | 38 + tests/phpunit.xml | 22 + 18 files changed, 6402 insertions(+), 113 deletions(-) create mode 100644 .php-cs-fixer.php create mode 100644 lib/Filesystem/RuntimeException.php create mode 100755 test create mode 100644 tests/bootstrap.php create mode 100644 tests/cases/TestExceptions.php create mode 100644 tests/cases/TestFilesystem.php create mode 100644 tests/cases/TestPath.php create mode 100644 tests/lib/FilesystemTestCase.php create mode 100644 tests/lib/MockStream.php create mode 100644 tests/phpunit.xml diff --git a/.gitignore b/.gitignore index e560c26..7feceb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Project-specific /test*.* +/tests/.phpunit.cache /build +.php-cs-fixer.cache +/symfony # General *.DS_Store diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..175b4c9 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,49 @@ +setRules([ + '@PSR12' => true, + 'array_indentation' => true, + 'array_syntax' => [ 'syntax' => 'short' ], + 'blank_line_after_namespace' => false, + 'blank_line_after_opening_tag' => false, + 'blank_lines_before_namespace' => false, + 'braces' => [ + 'allow_single_line_closure' => true, + 'position_after_functions_and_oop_constructs' => 'same' + ], + 'braces_position' => [ + 'classes_opening_brace' => 'same_line', + 'functions_opening_brace' => 'same_line' + ], + 'class_attributes_separation' => [ 'elements' => [ 'method' => 'one' ] ], + 'combine_consecutive_unsets' => true, + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => true, + 'function_typehint_space' => true, + 'general_phpdoc_annotation_remove' => [], + 'include' => true, + 'lowercase_cast' => true, + 'multiline_whitespace_before_semicolons' => false, + 'no_blank_lines_before_namespace' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'curly_brace_block', + 'extra', + 'throw', + 'use' + ] + ], + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_spaces_around_offset' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'object_operator_without_whitespace' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'ternary_operator_spaces' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + ]) + ->setLineEnding("\n") +; \ No newline at end of file diff --git a/composer.json b/composer.json index 479f033..f3f670e 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,11 @@ "MensBeam\\": "lib/" } }, + "autoload-dev": { + "psr-4": { + "MensBeam\\Filesystem\\Test\\": "tests/lib/" + } + }, "authors": [ { "name": "Dustin Wilson", @@ -20,7 +25,9 @@ "symfony/polyfill-mbstring": ">=1.8" }, "require-dev": { - "symfony/filesystem": ">=6.2" + "friendsofphp/php-cs-fixer": "^3.38", + "symfony/filesystem": "*", + "phpunit/phpunit": "^10.4" }, "suggest": { "ext-ctype": "For better performance", diff --git a/composer.lock b/composer.lock index 9e91329..c5db32b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b0fa1aa6c1c5d012f24f5d52924896aa", + "content-hash": "2a2ff9775bd5d2f59b8e89425208f092", "packages": [ { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -32,7 +32,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -70,7 +70,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -86,20 +86,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -114,7 +114,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -153,7 +153,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -169,33 +169,3036 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" } ], "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-10-11T07:11:09+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.38.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "d872cdd543797ade030aaa307c0a4954a712e081" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/d872cdd543797ade030aaa307c0a4954a712e081", + "reference": "d872cdd543797ade030aaa307c0a4954a712e081", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "composer/xdebug-handler": "^3.0.3", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0 || ^5.0", + "symfony/console": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4 || ^6.0", + "symfony/polyfill-mbstring": "^1.27", + "symfony/polyfill-php80": "^1.27", + "symfony/polyfill-php81": "^1.27", + "symfony/process": "^5.4 || ^6.0", + "symfony/stopwatch": "^5.4 || ^6.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3 || ^2.0", + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.0", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.5.3", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy": "^1.16", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "symfony/phpunit-bridge": "^6.2.3", + "symfony/yaml": "^5.4 || ^6.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.38.2" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2023-11-14T00:19:22+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.17.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + }, + "time": "2023-08-13T19:53:39+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "355324ca4980b8916c18b9db29f3ef484078f26e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/355324ca4980b8916c18b9db29f3ef484078f26e", + "reference": "355324ca4980b8916c18b9db29f3ef484078f26e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-text-template": "^3.0", + "sebastian/code-unit-reverse-lookup": "^3.0", + "sebastian/complexity": "^3.0", + "sebastian/environment": "^6.0", + "sebastian/lines-of-code": "^2.0", + "sebastian/version": "^4.0", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-10-04T15:34:17+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "cacd8b9dd224efa8eb28beb69004126c7ca1a1a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/cacd8b9dd224efa8eb28beb69004126c7ca1a1a1", + "reference": "cacd8b9dd224efa8eb28beb69004126c7ca1a1a1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.5", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-invoker": "^4.0", + "phpunit/php-text-template": "^3.0", + "phpunit/php-timer": "^6.0", + "sebastian/cli-parser": "^2.0", + "sebastian/code-unit": "^2.0", + "sebastian/comparator": "^5.0", + "sebastian/diff": "^5.0", + "sebastian/environment": "^6.0", + "sebastian/exporter": "^5.1", + "sebastian/global-state": "^6.0.1", + "sebastian/object-enumerator": "^5.0", + "sebastian/recursion-context": "^5.0", + "sebastian/type": "^4.0", + "sebastian/version": "^4.0" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.4-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.4.2" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-10-26T07:21:45+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", + "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:15+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-14T13:18:12+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68cfb347a44871f01e33ab0ef8215966432f6957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68cfb347a44871f01e33ab0ef8215966432f6957", + "reference": "68cfb347a44871f01e33ab0ef8215966432f6957", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.10", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-09-28T11:50:59+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-01T07:48:21+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-04-11T05:39:26+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-09-24T13:22:09+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/7ea9ead78f6d380d2a667864c132c2f7b83055e4", + "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-07-19T07:19:23+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "649e40d279e243d985aa8fb6e74dd5bb28dc185d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/649e40d279e243d985aa8fb6e74dd5bb28dc185d", + "reference": "649e40d279e243d985aa8fb6e74dd5bb28dc185d", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.10", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T09:25:50+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "symfony/console", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T08:09:35+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/adb01fe097a4ee930db9258a3cc906b5beb5cf2e", + "reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-06T06:56:43+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, { "name": "symfony/filesystem", - "version": "v6.2.5", + "version": "v6.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-06-01T08:30:39+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-26T12:56:25+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a10f19f5198d589d5c33333cffe98dc9820332dd", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-12T14:21:09+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/process", + "version": "v6.3.4", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "e59e8a4006afd7f5654786a83b4fcb8da98f4593" + "url": "https://github.com/symfony/process.git", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-07T10:39:22+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e59e8a4006afd7f5654786a83b4fcb8da98f4593", - "reference": "e59e8a4006afd7f5654786a83b4fcb8da98f4593", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, "autoload": { "psr-4": { - "Symfony\\Component\\Filesystem\\": "" + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2", + "reference": "fc47f1015ec80927ff64ba9094dfe8b9d48fe9f2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -215,10 +3218,96 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides basic utilities for the filesystem", + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-16T10:14:28+00:00" + }, + { + "name": "symfony/string", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "13880a87790c76ef994c91e87efb96134522577a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/13880a87790c76ef994c91e87efb96134522577a", + "reference": "13880a87790c76ef994c91e87efb96134522577a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/intl": "^6.2", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.2.5" + "source": "https://github.com/symfony/string/tree/v6.3.8" }, "funding": [ { @@ -234,7 +3323,57 @@ "type": "tidelift" } ], - "time": "2023-01-20T17:45:48+00:00" + "time": "2023-11-09T08:28:21+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" } ], "aliases": [], @@ -246,5 +3385,5 @@ "php": ">=8.1" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/lib/Filesystem.php b/lib/Filesystem.php index ec94879..d709c26 100644 --- a/lib/Filesystem.php +++ b/lib/Filesystem.php @@ -1,10 +1,10 @@ ['overwrite' => true]]))) { - throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile); + throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile) . self::$lastError, 0, null, $originFile); } $bytesCopied = stream_copy_to_stream($source, $target); @@ -80,6 +81,8 @@ class Filesystem { /** * Creates a directory recursively. * + * @return void + * * @throws IOException On any directory creation failure */ public static function mkdir(string|iterable $dirs, int $mode = 0777) { @@ -89,7 +92,7 @@ class Filesystem { } if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) { - throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir); + throw new IOException(sprintf('Failed to create "%s": ', $dir) . self::$lastError, 0, null, $dir); } } } @@ -119,12 +122,14 @@ class Filesystem { * @param int|null $time The touch time as a Unix timestamp, if not supplied the current system time is used * @param int|null $atime The access time as a Unix timestamp, if not supplied the current system time is used * + * @return void + * * @throws IOException When touch fails */ public static function touch(string|iterable $files, int $time = null, int $atime = null) { foreach (self::toIterable($files) as $file) { if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) { - throw new IOException(sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(sprintf('Failed to touch "%s": ', $file) . self::$lastError, 0, null, $file); } } } @@ -132,6 +137,8 @@ class Filesystem { /** * Removes files or directories. * + * @return void + * * @throws IOException When removal fails */ public static function remove(string|iterable $files) { @@ -147,14 +154,15 @@ class Filesystem { private static function doRemove(array $files, bool $isRecursive): void { $files = array_reverse($files); foreach ($files as $file) { + $file = (string)$file; if (is_link($file)) { // See https://bugs.php.net/52176 if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) { - throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError); + throw new IOException(sprintf('Failed to remove symlink "%s": ', $file) . self::$lastError); } } elseif (is_dir($file)) { if (!$isRecursive) { - $tmpName = \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-.')); + $tmpName = \dirname(realpath($file)) . '/.' . strrev(strtr(base64_encode(random_bytes(2)), '/=', '-_')); if (file_exists($tmpName)) { try { @@ -181,10 +189,10 @@ class Filesystem { $file = $origFile; } - throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError); + throw new IOException(sprintf('Failed to remove directory "%s": ', $file) . $lastError); } } elseif (!self::box('unlink', $file) && (str_contains(self::$lastError, 'Permission denied') || file_exists($file))) { - throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError); + throw new IOException(sprintf('Failed to remove file "%s": ', $file) . self::$lastError); } } } @@ -196,12 +204,15 @@ class Filesystem { * @param int $umask The mode mask (octal) * @param bool $recursive Whether change the mod recursively or not * + * @return void + * * @throws IOException When the change fails */ public static function chmod(string|iterable $files, int $mode, int $umask = 0000, bool $recursive = false) { foreach (self::toIterable($files) as $file) { - if (\is_int($mode) && !self::box('chmod', $file, $mode & ~$umask)) { - throw new IOException(sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file); + $file = (string)$file; + if (!self::box('chmod', $file, $mode & ~$umask)) { + throw new IOException(sprintf('Failed to chmod file "%s": ', $file) . self::$lastError, 0, null, $file); } if ($recursive && is_dir($file) && !is_link($file)) { self::chmod(new \FilesystemIterator($file), $mode, $umask, true); @@ -215,20 +226,23 @@ class Filesystem { * @param string|int $user A user name or number * @param bool $recursive Whether change the owner recursively or not * + * @return void + * * @throws IOException When the change fails */ public static function chown(string|iterable $files, string|int $user, bool $recursive = false) { foreach (self::toIterable($files) as $file) { + $file = (string)$file; if ($recursive && is_dir($file) && !is_link($file)) { self::chown(new \FilesystemIterator($file), $user, true); } if (is_link($file) && \function_exists('lchown')) { if (!self::box('lchown', $file, $user)) { - throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(sprintf('Failed to chown file "%s": ', $file) . self::$lastError, 0, null, $file); } } else { if (!self::box('chown', $file, $user)) { - throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(sprintf('Failed to chown file "%s": ', $file) . self::$lastError, 0, null, $file); } } } @@ -240,20 +254,23 @@ class Filesystem { * @param string|int $group A group name or number * @param bool $recursive Whether change the group recursively or not * + * @return void + * * @throws IOException When the change fails */ public static function chgrp(string|iterable $files, string|int $group, bool $recursive = false) { foreach (self::toIterable($files) as $file) { + $file = (string)$file; if ($recursive && is_dir($file) && !is_link($file)) { self::chgrp(new \FilesystemIterator($file), $group, true); } if (is_link($file) && \function_exists('lchgrp')) { if (!self::box('lchgrp', $file, $group)) { - throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(sprintf('Failed to chgrp file "%s": ', $file) . self::$lastError, 0, null, $file); } } else { if (!self::box('chgrp', $file, $group)) { - throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); + throw new IOException(sprintf('Failed to chgrp file "%s": ', $file) . self::$lastError, 0, null, $file); } } } @@ -262,6 +279,8 @@ class Filesystem { /** * Renames a file or a directory. * + * @return void + * * @throws IOException When target file or directory already exists * @throws IOException When origin cannot be renamed */ @@ -279,7 +298,7 @@ class Filesystem { return; } - throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target); + throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target) . self::$lastError, 0, null, $target); } } @@ -301,6 +320,8 @@ class Filesystem { /** * Creates a symbolic link or copy a directory. * + * @return void + * * @throws IOException When symlink fails */ public static function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false) { @@ -336,6 +357,8 @@ class Filesystem { * * @param string|string[] $targetFiles The target file(s) * + * @return void + * * @throws FileNotFoundException When original file is missing or not a file * @throws IOException When link fails, including if link already exists */ @@ -367,13 +390,13 @@ class Filesystem { /** * @param string $linkType Name of the link type, typically 'symbolic' or 'hard' */ - private static function linkException(string $origin, string $target, string $linkType) { + private static function linkException(string $origin, string $target, string $linkType): never { if (self::$lastError) { if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError, 'error code(1314)')) { throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target); } } - throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target); + throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target) . self::$lastError, 0, null, $target); } /** @@ -421,11 +444,9 @@ class Filesystem { $startPath = str_replace('\\', '/', $startPath); } - $splitDriveLetter = function ($path) { - return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0])) - ? [substr($path, 2), strtoupper($path[0])] - : [$path, null]; - }; + $splitDriveLetter = fn ($path) => (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0])) + ? [substr($path, 2), strtoupper($path[0])] + : [$path, null]; $splitPath = function ($path) { $result = []; @@ -449,7 +470,7 @@ class Filesystem { if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) { // End path is on another drive, so no relative path exists - return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : ''); + return $endDriveLetter . ':/' . ($endPathArr ? implode('/', $endPathArr) . '/' : ''); } // Find for which directory the common path stops @@ -471,7 +492,7 @@ class Filesystem { $endPathRemainder = implode('/', \array_slice($endPathArr, $index)); // Construct $endPath from traversing to the common path, then to the remaining $endPath - $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : ''); + $relativePath = $traverser . ('' !== $endPathRemainder ? $endPathRemainder . '/' : ''); return '' === $relativePath ? './' : $relativePath; } @@ -491,6 +512,8 @@ class Filesystem { * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false) * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false) * + * @return void + * * @throws IOException When file type is unknown */ public static function mirror(string $originDir, string $targetDir, \Traversable $iterator = null, array $options = []) { @@ -511,9 +534,9 @@ class Filesystem { } $targetDirLen = \strlen($targetDir); foreach ($deleteIterator as $file) { - $origin = $originDir.substr($file->getPathname(), $targetDirLen); + $origin = $originDir . substr($file->getPathname(), $targetDirLen); if (!self::exists($origin)) { - self::remove($file); + self::remove((string)$file); } } } @@ -533,15 +556,15 @@ class Filesystem { continue; } - $target = $targetDir.substr($file->getPathname(), $originDirLen); + $target = $targetDir . substr($file->getPathname(), $originDirLen); $filesCreatedWhileMirroring[$target] = true; - if (!$copyOnWindows && is_link($file)) { + if (!$copyOnWindows && is_link((string)$file)) { self::symlink($file->getLinkTarget(), $target); - } elseif (is_dir($file)) { + } elseif (is_dir((string)$file)) { self::mkdir($target); - } elseif (is_file($file)) { - self::copy($file, $target, $options['override'] ?? false); + } elseif (is_file((string)$file)) { + self::copy((string)$file, $target, $options['override'] ?? false); } else { throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); } @@ -552,8 +575,10 @@ class Filesystem { * Returns whether the file path is an absolute path. */ public static function isAbsolutePath(string $file): bool { - return '' !== $file && (strspn($file, '/\\', 0, 1) - || (\strlen($file) > 3 && ctype_alpha($file[0]) + return '' !== $file && ( + strspn($file, '/\\', 0, 1) + || ( + \strlen($file) > 3 && ctype_alpha($file[0]) && ':' === $file[1] && strspn($file, '/\\', 2, 1) ) @@ -578,19 +603,19 @@ class Filesystem { // If tempnam failed or no scheme return the filename otherwise prepend the scheme if ($tmpFile = self::box('tempnam', $hierarchy, $prefix)) { if (null !== $scheme && 'gs' !== $scheme) { - return $scheme.'://'.$tmpFile; + return $scheme . '://' . $tmpFile; } return $tmpFile; } - throw new IOException('A temporary file could not be created: '.self::$lastError); + throw new IOException('A temporary file could not be created: ' . self::$lastError); } // Loop until we create a valid temp file or have reached 10 attempts for ($i = 0; $i < 10; ++$i) { // Create a unique filename - $tmpFile = $dir.'/'.$prefix.uniqid((string)mt_rand(), true).$suffix; + $tmpFile = $dir . '/' . $prefix . uniqid((string)mt_rand(), true) . $suffix; // Use fopen instead of file_exists as some streams do not support stat // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability @@ -604,7 +629,7 @@ class Filesystem { return $tmpFile; } - throw new IOException('A temporary file could not be created: '.self::$lastError); + throw new IOException('A temporary file could not be created: ' . self::$lastError); } /** @@ -612,6 +637,8 @@ class Filesystem { * * @param string|resource $content The data to write into the file * + * @return void + * * @throws IOException if the file cannot be written to */ public static function dumpFile(string $filename, $content) { @@ -621,6 +648,12 @@ class Filesystem { $dir = \dirname($filename); + if (is_link($filename) && $linkTarget = self::readlink($filename)) { + self::dumpFile(Path::makeAbsolute($linkTarget, $dir), $content); + + return; + } + if (!is_dir($dir)) { self::mkdir($dir); } @@ -631,7 +664,7 @@ class Filesystem { try { if (false === self::box('file_put_contents', $tmpFile, $content)) { - throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); + throw new IOException(sprintf('Failed to write file "%s": ', $filename) . self::$lastError, 0, null, $filename); } self::box('chmod', $tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask()); @@ -650,6 +683,8 @@ class Filesystem { * @param string|resource $content The content to append * @param bool $lock Whether the file should be locked when writing to it * + * @return void + * * @throws IOException If the file is not writable */ public static function appendToFile(string $filename, $content/* , bool $lock = false */) { @@ -666,7 +701,7 @@ class Filesystem { $lock = \func_num_args() > 2 && func_get_arg(2); if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND | ($lock ? \LOCK_EX : 0))) { - throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); + throw new IOException(sprintf('Failed to write file "%s": ', $filename) . self::$lastError, 0, null, $filename); } } @@ -693,7 +728,7 @@ class Filesystem { self::assertFunctionExists($func); self::$lastError = null; - set_error_handler(__CLASS__.'::handleError'); + set_error_handler(__CLASS__ . '::handleError'); try { return $func(...$args); } finally { @@ -704,10 +739,10 @@ class Filesystem { /** * @internal */ - public static function handleError(int $type, string $msg) { + public static function handleError(int $type, string $msg): void { self::$lastError = $msg; } - - private function __construct() {} + private function __construct() { + } } diff --git a/lib/Filesystem/FileNotFoundException.php b/lib/Filesystem/FileNotFoundException.php index d9fccbf..867fd32 100644 --- a/lib/Filesystem/FileNotFoundException.php +++ b/lib/Filesystem/FileNotFoundException.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace MensBeam\Filesystem; - /** * Exception class thrown when a file couldn't be found. * @@ -28,4 +27,4 @@ class FileNotFoundException extends IOException { parent::__construct($message, $code, $previous, $path); } -} \ No newline at end of file +} diff --git a/lib/Filesystem/IOException.php b/lib/Filesystem/IOException.php index a5620e8..dd9d4c1 100644 --- a/lib/Filesystem/IOException.php +++ b/lib/Filesystem/IOException.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace MensBeam\Filesystem; - /** * Exception class thrown when a filesystem operation failure happens. * @@ -29,4 +28,4 @@ class IOException extends \RuntimeException { public function getPath(): ?string { return $this->path; } -} \ No newline at end of file +} diff --git a/lib/Filesystem/InvalidArgumentException.php b/lib/Filesystem/InvalidArgumentException.php index f3806b5..9a4e877 100644 --- a/lib/Filesystem/InvalidArgumentException.php +++ b/lib/Filesystem/InvalidArgumentException.php @@ -9,8 +9,5 @@ declare(strict_types=1); namespace MensBeam\Filesystem; - -/** - * @author Christian Flothmann - */ -class InvalidArgumentException extends \InvalidArgumentException {} \ No newline at end of file +class InvalidArgumentException extends \InvalidArgumentException { +} diff --git a/lib/Filesystem/RuntimeException.php b/lib/Filesystem/RuntimeException.php new file mode 100644 index 0000000..bb46707 --- /dev/null +++ b/lib/Filesystem/RuntimeException.php @@ -0,0 +1,13 @@ += Windows8 support if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) { - return self::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH')); + return self::canonicalize(getenv('HOMEDRIVE') . getenv('HOMEPATH')); } - throw new \RuntimeException("Cannot find the home directory path: Your environment or operating system isn't supported."); + throw new RuntimeException("Cannot find the home directory path: Your environment or operating system isn't supported."); } /** @@ -220,7 +223,7 @@ class Path { // UNIX root "/" or "\" (Windows style) if ('/' === $firstCharacter || '\\' === $firstCharacter) { - return $scheme.'/'; + return $scheme . '/'; } $length = \strlen($path); @@ -229,12 +232,12 @@ class Path { if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) { // Special case: "C:" if (2 === $length) { - return $scheme.$path.'/'; + return $scheme . $path . '/'; } // Normal case: "C:/ or "C:\" if ('/' === $path[2] || '\\' === $path[2]) { - return $scheme.$firstCharacter.$path[1].'/'; + return $scheme . $firstCharacter . $path[1] . '/'; } } @@ -341,10 +344,10 @@ class Path { // No actual extension in path if (empty($actualExtension)) { - return $path.('.' === substr($path, -1) ? '' : '.').$extension; + return $path . ('.' === substr($path, -1) ? '' : '.') . $extension; } - return substr($path, 0, -\strlen($actualExtension)).$extension; + return substr($path, 0, -\strlen($actualExtension)) . $extension; } public static function isAbsolute(string $path): bool { @@ -417,17 +420,17 @@ class Path { * * @param string $basePath an absolute base path * - * @throws \InvalidArgumentException if the base path is not absolute or if + * @throws InvalidArgumentException if the base path is not absolute or if * the given path is an absolute path with * a different root than the base path */ public static function makeAbsolute(string $path, string $basePath): string { if ('' === $basePath) { - throw new \InvalidArgumentException(sprintf('The base path must be a non-empty string. Got: "%s".', $basePath)); + throw new InvalidArgumentException(sprintf('The base path must be a non-empty string. Got: "%s".', $basePath)); } if (!self::isAbsolute($basePath)) { - throw new \InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath)); + throw new InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath)); } if (self::isAbsolute($path)) { @@ -441,7 +444,7 @@ class Path { $scheme = ''; } - return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path); + return $scheme . self::canonicalize(rtrim($basePath, '/\\') . '/' . $path); } /** @@ -516,12 +519,12 @@ class Path { // If the passed path is absolute, but the base path is not, we // cannot generate a relative path if ('' !== $root && '' === $baseRoot) { - throw new \InvalidArgumentException(sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath)); + throw new InvalidArgumentException(sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath)); } // Fail if the roots of the two paths are different if ($baseRoot && $root !== $baseRoot) { - throw new \InvalidArgumentException(sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot)); + throw new InvalidArgumentException(sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot)); } if ('' === $relativeBasePath) { @@ -548,7 +551,7 @@ class Path { $dotDotPrefix .= '../'; } - return rtrim($dotDotPrefix.implode('/', $parts), '/'); + return rtrim($dotDotPrefix . implode('/', $parts), '/'); } /** @@ -618,7 +621,7 @@ class Path { // Prevent false positives for common prefixes // see isBasePath() - if (str_starts_with($path.'/', $basePath.'/')) { + if (str_starts_with($path . '/', $basePath . '/')) { // next path continue 2; } @@ -627,7 +630,7 @@ class Path { } } - return $bpRoot.$basePath; + return $bpRoot . $basePath; } /** @@ -695,7 +698,7 @@ class Path { // Don't append a slash for the root "/", because then that root // won't be discovered as common prefix ("//" is not a prefix of // "/foobar/"). - return str_starts_with($ofPath.'/', rtrim($basePath, '/').'/'); + return str_starts_with($ofPath . '/', rtrim($basePath, '/') . '/'); } /** @@ -768,7 +771,7 @@ class Path { } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { if (2 === $length) { // Windows special case: "C:" - $root .= $path.'/'; + $root .= $path . '/'; $path = ''; } elseif ('/' === $path[2]) { // Windows normal case: "C:/".. @@ -788,5 +791,6 @@ class Path { return strtolower($string); } - private function __construct() {} + private function __construct() { + } } diff --git a/test b/test new file mode 100755 index 0000000..04c5b78 --- /dev/null +++ b/test @@ -0,0 +1,38 @@ +#!/usr/bin/env php + $v) { + if (in_array($v, ['--coverage', '--coverage-html'])) { + $argv[$k] = '--coverage-html tests/coverage'; + } +} + +$cmd = [ + $php, + '-d opcache.enable_cli=0', +]; + +if (!extension_loaded('xdebug')) { + $cmd[] = '-d zend_extension=xdebug.so'; +} + +$cmd = implode(' ', [ + ...$cmd, + '-d xdebug.mode=coverage,develop,trace', + escapeshellarg(__DIR__ . '/vendor/bin/phpunit'), + '--configuration tests/phpunit.xml', + ...$argv, + '--display-deprecations' +]); +passthru($cmd); \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..ae731e5 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,18 @@ +assertEquals('/foo', $e->getPath(), 'The pass should be returned.'); + } + + public function testGeneratedMessage() { + $e = new FileNotFoundException(null, 0, null, '/foo'); + $this->assertEquals('/foo', $e->getPath()); + $this->assertEquals('File "/foo" could not be found.', $e->getMessage(), 'A message should be generated.'); + } + + public function testGeneratedMessageWithoutPath() { + $e = new FileNotFoundException(); + $this->assertEquals('File could not be found.', $e->getMessage(), 'A message should be generated.'); + } + + public function testCustomMessage() { + $e = new FileNotFoundException('bar', 0, null, '/foo'); + $this->assertEquals('bar', $e->getMessage(), 'A custom message should be possible still.'); + } +} diff --git a/tests/cases/TestFilesystem.php b/tests/cases/TestFilesystem.php new file mode 100644 index 0000000..6dd70fd --- /dev/null +++ b/tests/cases/TestFilesystem.php @@ -0,0 +1,1731 @@ +workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($sourceFilePath, 'SOURCE FILE'); + + Fs::copy($sourceFilePath, $targetFilePath); + + $this->assertFileExists($targetFilePath); + $this->assertStringEqualsFile($targetFilePath, 'SOURCE FILE'); + } + + public function testCopyFails() { + $this->expectException(IOException::class); + $sourceFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + Fs::copy($sourceFilePath, $targetFilePath); + } + + public function testCopyUnreadableFileFails() { + $this->expectException(IOException::class); + // skip test on Windows; PHP can't easily set file as unreadable on Windows + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test cannot run on Windows.'); + } + + if (!getenv('USER') || 'root' === getenv('USER')) { + $this->markTestSkipped('This test will fail if run under superuser'); + } + + $sourceFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($sourceFilePath, 'SOURCE FILE'); + + // make sure target cannot be read + Fs::chmod($sourceFilePath, 0222); + + Fs::copy($sourceFilePath, $targetFilePath); + } + + public function testCopyOverridesExistingFileIfModified() { + $sourceFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($sourceFilePath, 'SOURCE FILE'); + file_put_contents($targetFilePath, 'TARGET FILE'); + touch($targetFilePath, time() - 1000); + + Fs::copy($sourceFilePath, $targetFilePath); + + $this->assertFileExists($targetFilePath); + $this->assertStringEqualsFile($targetFilePath, 'SOURCE FILE'); + } + + public function testCopyDoesNotOverrideExistingFileByDefault() { + $sourceFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($sourceFilePath, 'SOURCE FILE'); + file_put_contents($targetFilePath, 'TARGET FILE'); + + // make sure both files have the same modification time + $modificationTime = time() - 1000; + touch($sourceFilePath, $modificationTime); + touch($targetFilePath, $modificationTime); + + Fs::copy($sourceFilePath, $targetFilePath); + + $this->assertFileExists($targetFilePath); + $this->assertStringEqualsFile($targetFilePath, 'TARGET FILE'); + } + + public function testCopyOverridesExistingFileIfForced() { + $sourceFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($sourceFilePath, 'SOURCE FILE'); + file_put_contents($targetFilePath, 'TARGET FILE'); + + // make sure both files have the same modification time + $modificationTime = time() - 1000; + touch($sourceFilePath, $modificationTime); + touch($targetFilePath, $modificationTime); + + Fs::copy($sourceFilePath, $targetFilePath, true); + + $this->assertFileExists($targetFilePath); + $this->assertStringEqualsFile($targetFilePath, 'SOURCE FILE'); + } + + public function testCopyWithOverrideWithReadOnlyTargetFails() { + $this->expectException(IOException::class); + // skip test on Windows; PHP can't easily set file as unwritable on Windows + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test cannot run on Windows.'); + } + + if (!getenv('USER') || 'root' === getenv('USER')) { + $this->markTestSkipped('This test will fail if run under superuser'); + } + + $sourceFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($sourceFilePath, 'SOURCE FILE'); + file_put_contents($targetFilePath, 'TARGET FILE'); + + // make sure both files have the same modification time + $modificationTime = time() - 1000; + touch($sourceFilePath, $modificationTime); + touch($targetFilePath, $modificationTime); + + // make sure target is read-only + Fs::chmod($targetFilePath, 0444); + + Fs::copy($sourceFilePath, $targetFilePath, true); + } + + public function testCopyCreatesTargetDirectoryIfItDoesNotExist() { + $sourceFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFileDirectory = $this->workspace . \DIRECTORY_SEPARATOR . 'directory'; + $targetFilePath = $targetFileDirectory . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($sourceFilePath, 'SOURCE FILE'); + + Fs::copy($sourceFilePath, $targetFilePath); + + $this->assertDirectoryExists($targetFileDirectory); + $this->assertFileExists($targetFilePath); + $this->assertStringEqualsFile($targetFilePath, 'SOURCE FILE'); + } + + /** + * @group network + */ + public function testCopyForOriginUrlsAndExistingLocalFileDefaultsToCopy() { + if (!\in_array('https', stream_get_wrappers())) { + $this->markTestSkipped('"https" stream wrapper is not enabled.'); + } + $sourceFilePath = 'https://symfony.com/images/common/logo/logo_symfony_header.png'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($targetFilePath, 'TARGET FILE'); + + Fs::copy($sourceFilePath, $targetFilePath, false); + + $this->assertFileExists($targetFilePath); + $this->assertEquals(file_get_contents($sourceFilePath), file_get_contents($targetFilePath)); + } + + public function testMkdirCreatesDirectoriesRecursively() { + $directory = $this->workspace + . \DIRECTORY_SEPARATOR . 'directory' + . \DIRECTORY_SEPARATOR . 'sub_directory'; + + Fs::mkdir($directory); + + $this->assertDirectoryExists($directory); + } + + public function testMkdirCreatesDirectoriesFromArray() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + $directories = [ + $basePath . '1', $basePath . '2', $basePath . '3', + ]; + + Fs::mkdir($directories); + + $this->assertDirectoryExists($basePath . '1'); + $this->assertDirectoryExists($basePath . '2'); + $this->assertDirectoryExists($basePath . '3'); + } + + public function testMkdirCreatesDirectoriesFromTraversableObject() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + $directories = new \ArrayObject([ + $basePath . '1', $basePath . '2', $basePath . '3', + ]); + + Fs::mkdir($directories); + + $this->assertDirectoryExists($basePath . '1'); + $this->assertDirectoryExists($basePath . '2'); + $this->assertDirectoryExists($basePath . '3'); + } + + public function testMkdirCreatesDirectoriesFails() { + $this->expectException(IOException::class); + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + $dir = $basePath . '2'; + + file_put_contents($dir, ''); + + Fs::mkdir($dir); + } + + public function testTouchCreatesEmptyFile() { + $file = $this->workspace . \DIRECTORY_SEPARATOR . '1'; + + Fs::touch($file); + + $this->assertFileExists($file); + } + + public function testTouchFails() { + $this->expectException(IOException::class); + $file = $this->workspace . \DIRECTORY_SEPARATOR . '1' . \DIRECTORY_SEPARATOR . '2'; + + Fs::touch($file); + } + + public function testTouchCreatesEmptyFilesFromArray() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + $files = [ + $basePath . '1', $basePath . '2', $basePath . '3', + ]; + + Fs::touch($files); + + $this->assertFileExists($basePath . '1'); + $this->assertFileExists($basePath . '2'); + $this->assertFileExists($basePath . '3'); + } + + public function testTouchCreatesEmptyFilesFromTraversableObject() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + $files = new \ArrayObject([ + $basePath . '1', $basePath . '2', $basePath . '3', + ]); + + Fs::touch($files); + + $this->assertFileExists($basePath . '1'); + $this->assertFileExists($basePath . '2'); + $this->assertFileExists($basePath . '3'); + } + + public function testRemoveCleansFilesAndDirectoriesIteratively() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR . 'directory' . \DIRECTORY_SEPARATOR; + + mkdir($basePath); + mkdir($basePath . 'dir'); + touch($basePath . 'file'); + + Fs::remove($basePath); + + $this->assertFileDoesNotExist($basePath); + } + + public function testRemoveCleansArrayOfFilesAndDirectories() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + + mkdir($basePath . 'dir'); + touch($basePath . 'file'); + + $files = [ + $basePath . 'dir', $basePath . 'file', + ]; + + Fs::remove($files); + + $this->assertFileDoesNotExist($basePath . 'dir'); + $this->assertFileDoesNotExist($basePath . 'file'); + } + + public function testRemoveCleansTraversableObjectOfFilesAndDirectories() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + + mkdir($basePath . 'dir'); + touch($basePath . 'file'); + + $files = new \ArrayObject([ + $basePath . 'dir', $basePath . 'file', + ]); + + Fs::remove($files); + + $this->assertFileDoesNotExist($basePath . 'dir'); + $this->assertFileDoesNotExist($basePath . 'file'); + } + + public function testRemoveIgnoresNonExistingFiles() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + + mkdir($basePath . 'dir'); + + $files = [ + $basePath . 'dir', $basePath . 'file', + ]; + + Fs::remove($files); + + $this->assertFileDoesNotExist($basePath . 'dir'); + } + + public function testRemoveThrowsExceptionOnPermissionDenied() { + $this->markAsSkippedIfChmodIsMissing(); + + $basePath = $this->workspace . \DIRECTORY_SEPARATOR . 'dir_permissions'; + mkdir($basePath); + $file = $basePath . \DIRECTORY_SEPARATOR . 'file'; + touch($file); + chmod($basePath, 0400); + + try { + Fs::remove($file); + $this->fail('Filesystem::remove() should throw an exception'); + } catch (IOException $e) { + $this->assertStringContainsString('Failed to remove file "' . $file . '"', $e->getMessage()); + $this->assertStringContainsString('Permission denied', $e->getMessage()); + } finally { + // Make sure we can clean up this file + chmod($basePath, 0777); + } + } + + public function testRemoveCleansInvalidLinks() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $basePath = $this->workspace . \DIRECTORY_SEPARATOR . 'directory' . \DIRECTORY_SEPARATOR; + + mkdir($basePath); + mkdir($basePath . 'dir'); + // create symlink to nonexistent file + @symlink($basePath . 'file', $basePath . 'file-link'); + + // create symlink to dir using trailing forward slash + Fs::symlink($basePath . 'dir/', $basePath . 'dir-link'); + $this->assertDirectoryExists($basePath . 'dir-link'); + + // create symlink to nonexistent dir + rmdir($basePath . 'dir'); + $this->assertDirectoryDoesNotExist($basePath . 'dir-link'); + + Fs::remove($basePath); + + $this->assertFileDoesNotExist($basePath); + } + + public function testFilesExists() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR . 'directory' . \DIRECTORY_SEPARATOR; + + mkdir($basePath); + touch($basePath . 'file1'); + mkdir($basePath . 'folder'); + + $this->assertTrue(Fs::exists($basePath . 'file1')); + $this->assertTrue(Fs::exists($basePath . 'folder')); + } + + public function testFilesExistsFails() { + $this->expectException(IOException::class); + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Long file names are an issue on Windows'); + } + $basePath = $this->workspace . '\\directory\\'; + $maxPathLength = \PHP_MAXPATHLEN - 2; + + $oldPath = getcwd(); + mkdir($basePath); + chdir($basePath); + $file = str_repeat('T', $maxPathLength - \strlen($basePath) + 1); + $path = $basePath . $file; + exec('TYPE NUL >>' . $file); // equivalent of touch, we cannot use the php touch() here because it suffers from the same limitation + $this->longPathNamesWindows[] = $path; // save this so we can clean up later + chdir($oldPath); + Fs::exists($path); + } + + public function testFilesExistsTraversableObjectOfFilesAndDirectories() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + + mkdir($basePath . 'dir'); + touch($basePath . 'file'); + + $files = new \ArrayObject([ + $basePath . 'dir', $basePath . 'file', + ]); + + $this->assertTrue(Fs::exists($files)); + } + + public function testFilesNotExistsTraversableObjectOfFilesAndDirectories() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR; + + mkdir($basePath . 'dir'); + touch($basePath . 'file'); + touch($basePath . 'file2'); + + $files = new \ArrayObject([ + $basePath . 'dir', $basePath . 'file', $basePath . 'file2', + ]); + + unlink($basePath . 'file'); + + $this->assertFalse(Fs::exists($files)); + } + + public function testInvalidFileNotExists() { + $basePath = $this->workspace . \DIRECTORY_SEPARATOR . 'directory' . \DIRECTORY_SEPARATOR; + + $this->assertFalse(Fs::exists($basePath . time())); + } + + public function testChmodChangesFileMode() { + $this->markAsSkippedIfChmodIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + $file = $dir . \DIRECTORY_SEPARATOR . 'file'; + touch($file); + + Fs::chmod($file, 0400); + Fs::chmod($dir, 0753); + + $this->assertFilePermissions(753, $dir); + $this->assertFilePermissions(400, $file); + } + + public function testChmodRecursive() { + $this->markAsSkippedIfChmodIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + $file = $dir . \DIRECTORY_SEPARATOR . 'file'; + touch($file); + + Fs::chmod($file, 0400, 0000, true); + Fs::chmod($dir, 0753, 0000, true); + + $this->assertFilePermissions(753, $dir); + $this->assertFilePermissions(753, $file); + } + + public function testChmodAppliesUmask() { + $this->markAsSkippedIfChmodIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + touch($file); + + Fs::chmod($file, 0770, 0022); + $this->assertFilePermissions(750, $file); + } + + public function testChmodChangesModeOfArrayOfFiles() { + $this->markAsSkippedIfChmodIsMissing(); + + $directory = $this->workspace . \DIRECTORY_SEPARATOR . 'directory'; + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $files = [$directory, $file]; + + mkdir($directory); + touch($file); + + Fs::chmod($files, 0753); + + $this->assertFilePermissions(753, $file); + $this->assertFilePermissions(753, $directory); + } + + public function testChmodChangesModeOfTraversableFileObject() { + $this->markAsSkippedIfChmodIsMissing(); + + $directory = $this->workspace . \DIRECTORY_SEPARATOR . 'directory'; + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $files = new \ArrayObject([$directory, $file]); + + mkdir($directory); + touch($file); + + Fs::chmod($files, 0753); + + $this->assertFilePermissions(753, $file); + $this->assertFilePermissions(753, $directory); + } + + public function testChmodChangesZeroModeOnSubdirectoriesOnRecursive() { + $this->markAsSkippedIfChmodIsMissing(); + + $directory = $this->workspace . \DIRECTORY_SEPARATOR . 'directory'; + $subdirectory = $directory . \DIRECTORY_SEPARATOR . 'subdirectory'; + + mkdir($directory); + mkdir($subdirectory); + chmod($subdirectory, 0000); + + Fs::chmod($directory, 0753, 0000, true); + + $this->assertFilePermissions(753, $subdirectory); + } + + public function testChownByName() { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + + $owner = $this->getFileOwner($dir); + Fs::chown($dir, $owner); + + $this->assertSame($owner, $this->getFileOwner($dir)); + } + + public function testChownById() { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + + $ownerId = $this->getFileOwnerId($dir); + Fs::chown($dir, $ownerId); + + $this->assertSame($ownerId, $this->getFileOwnerId($dir)); + } + + public function testChownRecursiveByName() { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + $file = $dir . \DIRECTORY_SEPARATOR . 'file'; + touch($file); + + $owner = $this->getFileOwner($dir); + Fs::chown($dir, $owner, true); + + $this->assertSame($owner, $this->getFileOwner($file)); + } + + public function testChownRecursiveById() { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + $file = $dir . \DIRECTORY_SEPARATOR . 'file'; + touch($file); + + $ownerId = $this->getFileOwnerId($dir); + Fs::chown($dir, $ownerId, true); + + $this->assertSame($ownerId, $this->getFileOwnerId($file)); + } + + public function testChownSymlink() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::symlink($file, $link); + + $owner = $this->getFileOwner($link); + Fs::chown($link, $owner); + + $this->assertSame($owner, $this->getFileOwner($link)); + } + + public function testChownLink() { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::hardlink($file, $link); + + $owner = $this->getFileOwner($link); + Fs::chown($link, $owner); + + $this->assertSame($owner, $this->getFileOwner($link)); + } + + public function testChownSymlinkFails() { + $this->expectException(IOException::class); + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::symlink($file, $link); + + Fs::chown($link, 'user' . time() . mt_rand(1000, 9999)); + } + + public function testChownLinkFails() { + $this->expectException(IOException::class); + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::hardlink($file, $link); + + Fs::chown($link, 'user' . time() . mt_rand(1000, 9999)); + } + + public function testChownFail() { + $this->expectException(IOException::class); + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + + Fs::chown($dir, 'user' . time() . mt_rand(1000, 9999)); + } + + public function testChgrpByName() { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + + $group = $this->getFileGroup($dir); + Fs::chgrp($dir, $group); + + $this->assertSame($group, $this->getFileGroup($dir)); + } + + public function testChgrpById() { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + + $groupId = $this->getFileGroupId($dir); + Fs::chgrp($dir, $groupId); + + $this->assertSame($groupId, $this->getFileGroupId($dir)); + } + + public function testChgrpRecursive() { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + $file = $dir . \DIRECTORY_SEPARATOR . 'file'; + touch($file); + + $group = $this->getFileGroup($dir); + Fs::chgrp($dir, $group, true); + + $this->assertSame($group, $this->getFileGroup($file)); + } + + public function testChgrpSymlinkByName() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::symlink($file, $link); + + $group = $this->getFileGroup($link); + Fs::chgrp($link, $group); + + $this->assertSame($group, $this->getFileGroup($link)); + } + + public function testChgrpSymlinkById() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::symlink($file, $link); + + $groupId = $this->getFileGroupId($link); + Fs::chgrp($link, $groupId); + + $this->assertSame($groupId, $this->getFileGroupId($link)); + } + + public function testChgrpLink() { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::hardlink($file, $link); + + $group = $this->getFileGroup($link); + Fs::chgrp($link, $group); + + $this->assertSame($group, $this->getFileGroup($link)); + } + + public function testChgrpSymlinkFails() { + $this->expectException(IOException::class); + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::symlink($file, $link); + + Fs::chgrp($link, 'user' . time() . mt_rand(1000, 9999)); + } + + public function testChgrpLinkFails() { + $this->expectException(IOException::class); + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::hardlink($file, $link); + + Fs::chgrp($link, 'user' . time() . mt_rand(1000, 9999)); + } + + public function testChgrpFail() { + $this->expectException(IOException::class); + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace . \DIRECTORY_SEPARATOR . 'dir'; + mkdir($dir); + + Fs::chgrp($dir, 'user' . time() . mt_rand(1000, 9999)); + } + + public function testRename() { + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $newPath = $this->workspace . \DIRECTORY_SEPARATOR . 'new_file'; + touch($file); + + Fs::rename($file, $newPath); + + $this->assertFileDoesNotExist($file); + $this->assertFileExists($newPath); + } + + public function testRenameThrowsExceptionIfTargetAlreadyExists() { + $this->expectException(IOException::class); + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $newPath = $this->workspace . \DIRECTORY_SEPARATOR . 'new_file'; + + touch($file); + touch($newPath); + + Fs::rename($file, $newPath); + } + + public function testRenameOverwritesTheTargetIfItAlreadyExists() { + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $newPath = $this->workspace . \DIRECTORY_SEPARATOR . 'new_file'; + + touch($file); + touch($newPath); + + Fs::rename($file, $newPath, true); + + $this->assertFileDoesNotExist($file); + $this->assertFileExists($newPath); + } + + public function testRenameThrowsExceptionOnError() { + $this->expectException(IOException::class); + $file = $this->workspace . \DIRECTORY_SEPARATOR . uniqid('fs_test_', true); + $newPath = $this->workspace . \DIRECTORY_SEPARATOR . 'new_file'; + + Fs::rename($file, $newPath); + } + + public function testSymlink() { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support creating "broken" symlinks'); + } + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + // $file does not exist right now: creating "broken" links is a wanted feature + Fs::symlink($file, $link); + + $this->assertTrue(is_link($link)); + + // Create the linked file AFTER creating the link + touch($file); + + $this->assertEquals($file, readlink($link)); + } + + /** + * @depends testSymlink + */ + public function testRemoveSymlink() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + Fs::remove($link); + + $this->assertFalse(is_link($link)); + $this->assertFalse(is_file($link)); + $this->assertDirectoryDoesNotExist($link); + } + + public function testSymlinkIsOverwrittenIfPointsToDifferentTarget() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + symlink($this->workspace, $link); + + Fs::symlink($file, $link); + + $this->assertTrue(is_link($link)); + $this->assertEquals($file, readlink($link)); + } + + public function testSymlinkIsNotOverwrittenIfAlreadyCreated() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + symlink($file, $link); + + Fs::symlink($file, $link); + + $this->assertTrue(is_link($link)); + $this->assertEquals($file, readlink($link)); + } + + public function testSymlinkCreatesTargetDirectoryIfItDoesNotExist() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link1 = $this->workspace . \DIRECTORY_SEPARATOR . 'dir' . \DIRECTORY_SEPARATOR . 'link'; + $link2 = $this->workspace . \DIRECTORY_SEPARATOR . 'dir' . \DIRECTORY_SEPARATOR . 'subdir' . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + Fs::symlink($file, $link1); + Fs::symlink($file, $link2); + + $this->assertTrue(is_link($link1)); + $this->assertEquals($file, readlink($link1)); + $this->assertTrue(is_link($link2)); + $this->assertEquals($file, readlink($link2)); + } + + public function testLink() { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + Fs::hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + /** + * @depends testLink + */ + public function testRemoveLink() { + $this->markAsSkippedIfLinkIsMissing(); + + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + Fs::remove($link); + + $this->assertTrue(!is_file($link)); + } + + public function testLinkIsOverwrittenIfPointsToDifferentTarget() { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $file2 = $this->workspace . \DIRECTORY_SEPARATOR . 'file2'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + touch($file2); + link($file2, $link); + + Fs::hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + public function testLinkIsNotOverwrittenIfAlreadyCreated() { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + link($file, $link); + + Fs::hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + public function testLinkWithSeveralTargets() { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link1 = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + $link2 = $this->workspace . \DIRECTORY_SEPARATOR . 'link2'; + + touch($file); + + Fs::hardlink($file, [$link1, $link2]); + + $this->assertTrue(is_file($link1)); + $this->assertEquals(fileinode($file), fileinode($link1)); + $this->assertTrue(is_file($link2)); + $this->assertEquals(fileinode($file), fileinode($link2)); + } + + public function testLinkWithSameTarget() { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace . \DIRECTORY_SEPARATOR . 'file'; + $link = $this->workspace . \DIRECTORY_SEPARATOR . 'link'; + + touch($file); + + // practically same as testLinkIsNotOverwrittenIfAlreadyCreated + Fs::hardlink($file, [$link, $link]); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + public function testReadRelativeLink() { + $this->markAsSkippedIfSymlinkIsMissing(); + + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Relative symbolic links are not supported on Windows'); + } + + $file = $this->workspace . '/file'; + $link1 = $this->workspace . '/dir/link'; + $link2 = $this->workspace . '/dir/link2'; + touch($file); + + Fs::symlink('../file', $link1); + Fs::symlink('link', $link2); + + $this->assertEquals($this->normalize('../file'), Fs::readlink($link1)); + $this->assertEquals('link', Fs::readlink($link2)); + $this->assertEquals($file, Fs::readlink($link1, true)); + $this->assertEquals($file, Fs::readlink($link2, true)); + $this->assertEquals($file, Fs::readlink($file, true)); + } + + public function testReadAbsoluteLink() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->normalize($this->workspace . '/file'); + $link1 = $this->normalize($this->workspace . '/dir/link'); + $link2 = $this->normalize($this->workspace . '/dir/link2'); + touch($file); + + Fs::symlink($file, $link1); + Fs::symlink($link1, $link2); + + $this->assertEquals($file, Fs::readlink($link1)); + + $this->assertEquals($link1, Fs::readlink($link2)); + $this->assertEquals($file, Fs::readlink($link1, true)); + $this->assertEquals($file, Fs::readlink($link2, true)); + $this->assertEquals($file, Fs::readlink($file, true)); + } + + public function testReadBrokenLink() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = Path::join($this->workspace, 'file'); + $link = Path::join($this->workspace, 'link'); + + touch($file); + Fs::symlink($file, $link); + Fs::remove($file); + + $this->assertEquals($file, Path::normalize(Fs::readlink($link))); + $this->assertNull(Fs::readlink($link, true)); + + touch($file); + $this->assertEquals($file, Path::normalize(Fs::readlink($link, true))); + } + + public function testReadLinkDefaultPathDoesNotExist() { + $this->assertNull(Fs::readlink($this->normalize($this->workspace . '/invalid'))); + } + + public function testReadLinkDefaultPathNotLink() { + $file = $this->normalize($this->workspace . '/file'); + touch($file); + + $this->assertNull(Fs::readlink($file)); + } + + public function testReadLinkCanonicalizePath() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->normalize($this->workspace . '/file'); + mkdir($this->normalize($this->workspace . '/dir')); + touch($file); + + $this->assertEquals($file, Fs::readlink($this->normalize($this->workspace . '/dir/../file'), true)); + } + + public function testReadLinkCanonicalizedPathDoesNotExist() { + $this->assertNull(Fs::readlink($this->normalize($this->workspace . 'invalid'), true)); + } + + /** + * @dataProvider providePathsForMakePathRelative + */ + public function testMakePathRelative($endPath, $startPath, $expectedPath) { + $path = Fs::makePathRelative($endPath, $startPath); + + $this->assertEquals($expectedPath, $path); + } + + public static function providePathsForMakePathRelative() { + $paths = [ + ['/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component', '../'], + ['/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component/', '../'], + ['/var/lib/symfony/src/Symfony', '/var/lib/symfony/src/Symfony/Component', '../'], + ['/var/lib/symfony/src/Symfony', '/var/lib/symfony/src/Symfony/Component/', '../'], + ['/usr/lib/symfony/', '/var/lib/symfony/src/Symfony/Component', '../../../../../../usr/lib/symfony/'], + ['/var/lib/symfony/src/Symfony/', '/var/lib/symfony/', 'src/Symfony/'], + ['/aa/bb', '/aa/bb', './'], + ['/aa/bb', '/aa/bb/', './'], + ['/aa/bb/', '/aa/bb', './'], + ['/aa/bb/', '/aa/bb/', './'], + ['/aa/bb/cc', '/aa/bb/cc/dd', '../'], + ['/aa/bb/cc', '/aa/bb/cc/dd/', '../'], + ['/aa/bb/cc/', '/aa/bb/cc/dd', '../'], + ['/aa/bb/cc/', '/aa/bb/cc/dd/', '../'], + ['/aa/bb/cc', '/aa', 'bb/cc/'], + ['/aa/bb/cc', '/aa/', 'bb/cc/'], + ['/aa/bb/cc/', '/aa', 'bb/cc/'], + ['/aa/bb/cc/', '/aa/', 'bb/cc/'], + ['/a/aab/bb', '/a/aa', '../aab/bb/'], + ['/a/aab/bb', '/a/aa/', '../aab/bb/'], + ['/a/aab/bb/', '/a/aa', '../aab/bb/'], + ['/a/aab/bb/', '/a/aa/', '../aab/bb/'], + ['/a/aab/bb/', '/', 'a/aab/bb/'], + ['/a/aab/bb/', '/b/aab', '../../a/aab/bb/'], + ['/aab/bb', '/aa', '../aab/bb/'], + ['/aab', '/aa', '../aab/'], + ['/aa/bb/cc', '/aa/dd/..', 'bb/cc/'], + ['/aa/../bb/cc', '/aa/dd/..', '../bb/cc/'], + ['/aa/bb/../../cc', '/aa/../dd/..', 'cc/'], + ['/../aa/bb/cc', '/aa/dd/..', 'bb/cc/'], + ['/../../aa/../bb/cc', '/aa/dd/..', '../bb/cc/'], + ['C:/aa/bb/cc', 'C:/aa/dd/..', 'bb/cc/'], + ['C:/aa/bb/cc', 'c:/aa/dd/..', 'bb/cc/'], + ['c:/aa/../bb/cc', 'c:/aa/dd/..', '../bb/cc/'], + ['C:/aa/bb/../../cc', 'C:/aa/../dd/..', 'cc/'], + ['C:/../aa/bb/cc', 'C:/aa/dd/..', 'bb/cc/'], + ['C:/../../aa/../bb/cc', 'C:/aa/dd/..', '../bb/cc/'], + ['D:/', 'C:/aa/../bb/cc', 'D:/'], + ['D:/aa/bb', 'C:/aa', 'D:/aa/bb/'], + ['D:/../../aa/../bb/cc', 'C:/aa/dd/..', 'D:/bb/cc/'], + ]; + + if ('\\' === \DIRECTORY_SEPARATOR) { + $paths[] = ['c:\var\lib/symfony/src/Symfony/', 'c:/var/lib/symfony/', 'src/Symfony/']; + } + + return $paths; + } + + public function testMakePathRelativeWithRelativeStartPath() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The start path "var/lib/symfony/src/Symfony/Component" is not absolute.'); + $this->assertSame('../../../', Fs::makePathRelative('/var/lib/symfony/', 'var/lib/symfony/src/Symfony/Component')); + } + + public function testMakePathRelativeWithRelativeEndPath() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The end path "var/lib/symfony/" is not absolute.'); + $this->assertSame('../../../', Fs::makePathRelative('var/lib/symfony/', '/var/lib/symfony/src/Symfony/Component')); + } + + public function testMirrorCopiesFilesAndDirectoriesRecursively() { + $sourcePath = $this->workspace . \DIRECTORY_SEPARATOR . 'source' . \DIRECTORY_SEPARATOR; + $directory = $sourcePath . 'directory' . \DIRECTORY_SEPARATOR; + $file1 = $directory . 'file1'; + $file2 = $sourcePath . 'file2'; + + mkdir($sourcePath); + mkdir($directory); + file_put_contents($file1, 'FILE1'); + file_put_contents($file2, 'FILE2'); + + $targetPath = $this->workspace . \DIRECTORY_SEPARATOR . 'target' . \DIRECTORY_SEPARATOR; + + Fs::mirror($sourcePath, $targetPath); + + $this->assertDirectoryExists($targetPath); + $this->assertDirectoryExists($targetPath . 'directory'); + $this->assertFileEquals($file1, $targetPath . 'directory' . \DIRECTORY_SEPARATOR . 'file1'); + $this->assertFileEquals($file2, $targetPath . 'file2'); + + Fs::remove($file1); + + Fs::mirror($sourcePath, $targetPath, null, ['delete' => false]); + $this->assertTrue(Fs::exists($targetPath . 'directory' . \DIRECTORY_SEPARATOR . 'file1')); + + Fs::mirror($sourcePath, $targetPath, null, ['delete' => true]); + $this->assertFalse(Fs::exists($targetPath . 'directory' . \DIRECTORY_SEPARATOR . 'file1')); + + file_put_contents($file1, 'FILE1'); + + Fs::mirror($sourcePath, $targetPath, null, ['delete' => true]); + $this->assertTrue(Fs::exists($targetPath . 'directory' . \DIRECTORY_SEPARATOR . 'file1')); + + Fs::remove($directory); + Fs::mirror($sourcePath, $targetPath, null, ['delete' => true]); + $this->assertFalse(Fs::exists($targetPath . 'directory')); + $this->assertFalse(Fs::exists($targetPath . 'directory' . \DIRECTORY_SEPARATOR . 'file1')); + } + + public function testMirrorCreatesEmptyDirectory() { + $sourcePath = $this->workspace . \DIRECTORY_SEPARATOR . 'source' . \DIRECTORY_SEPARATOR; + + mkdir($sourcePath); + + $targetPath = $this->workspace . \DIRECTORY_SEPARATOR . 'target' . \DIRECTORY_SEPARATOR; + + Fs::mirror($sourcePath, $targetPath); + + $this->assertDirectoryExists($targetPath); + + Fs::remove($sourcePath); + } + + public function testMirrorCopiesLinks() { + $this->markAsSkippedIfSymlinkIsMissing(); + + $sourcePath = $this->workspace . \DIRECTORY_SEPARATOR . 'source' . \DIRECTORY_SEPARATOR; + + mkdir($sourcePath); + file_put_contents($sourcePath . 'file1', 'FILE1'); + symlink($sourcePath . 'file1', $sourcePath . 'link1'); + + $targetPath = $this->workspace . \DIRECTORY_SEPARATOR . 'target' . \DIRECTORY_SEPARATOR; + + Fs::mirror($sourcePath, $targetPath); + + $this->assertDirectoryExists($targetPath); + $this->assertFileEquals($sourcePath . 'file1', $targetPath . 'link1'); + $this->assertTrue(is_link($targetPath . \DIRECTORY_SEPARATOR . 'link1')); + } + + public function testMirrorCopiesLinkedDirectoryContents() { + $this->markAsSkippedIfSymlinkIsMissing(true); + + $sourcePath = $this->workspace . \DIRECTORY_SEPARATOR . 'source' . \DIRECTORY_SEPARATOR; + + mkdir($sourcePath . 'nested/', 0777, true); + file_put_contents($sourcePath . '/nested/file1.txt', 'FILE1'); + // Note: We symlink directory, not file + symlink($sourcePath . 'nested', $sourcePath . 'link1'); + + $targetPath = $this->workspace . \DIRECTORY_SEPARATOR . 'target' . \DIRECTORY_SEPARATOR; + + Fs::mirror($sourcePath, $targetPath); + + $this->assertDirectoryExists($targetPath); + $this->assertFileEquals($sourcePath . '/nested/file1.txt', $targetPath . 'link1/file1.txt'); + $this->assertTrue(is_link($targetPath . \DIRECTORY_SEPARATOR . 'link1')); + } + + public function testMirrorCopiesRelativeLinkedContents() { + $this->markAsSkippedIfSymlinkIsMissing(true); + + $sourcePath = $this->workspace . \DIRECTORY_SEPARATOR . 'source' . \DIRECTORY_SEPARATOR; + $oldPath = getcwd(); + + mkdir($sourcePath . 'nested/', 0777, true); + file_put_contents($sourcePath . '/nested/file1.txt', 'FILE1'); + // Note: Create relative symlink + chdir($sourcePath); + symlink('nested', 'link1'); + + chdir($oldPath); + + $targetPath = $this->workspace . \DIRECTORY_SEPARATOR . 'target' . \DIRECTORY_SEPARATOR; + + Fs::mirror($sourcePath, $targetPath); + + $this->assertDirectoryExists($targetPath); + $this->assertFileEquals($sourcePath . '/nested/file1.txt', $targetPath . 'link1/file1.txt'); + $this->assertTrue(is_link($targetPath . \DIRECTORY_SEPARATOR . 'link1')); + $this->assertEquals('\\' === \DIRECTORY_SEPARATOR ? realpath($sourcePath . '\nested') : 'nested', readlink($targetPath . \DIRECTORY_SEPARATOR . 'link1')); + } + + public function testMirrorContentsWithSameNameAsSourceOrTargetWithoutDeleteOption() { + $sourcePath = $this->workspace . \DIRECTORY_SEPARATOR . 'source' . \DIRECTORY_SEPARATOR; + + mkdir($sourcePath); + touch($sourcePath . 'source'); + touch($sourcePath . 'target'); + + $targetPath = $this->workspace . \DIRECTORY_SEPARATOR . 'target' . \DIRECTORY_SEPARATOR; + + $oldPath = getcwd(); + chdir($this->workspace); + + Fs::mirror('source', $targetPath); + + chdir($oldPath); + + $this->assertDirectoryExists($targetPath); + $this->assertFileExists($targetPath . 'source'); + $this->assertFileExists($targetPath . 'target'); + } + + public function testMirrorContentsWithSameNameAsSourceOrTargetWithDeleteOption() { + $sourcePath = $this->workspace . \DIRECTORY_SEPARATOR . 'source' . \DIRECTORY_SEPARATOR; + + mkdir($sourcePath); + touch($sourcePath . 'source'); + + $targetPath = $this->workspace . \DIRECTORY_SEPARATOR . 'target' . \DIRECTORY_SEPARATOR; + + mkdir($targetPath); + touch($targetPath . 'source'); + touch($targetPath . 'target'); + + $oldPath = getcwd(); + chdir($this->workspace); + + Fs::mirror('source', 'target', null, ['delete' => true]); + + chdir($oldPath); + + $this->assertDirectoryExists($targetPath); + $this->assertFileExists($targetPath . 'source'); + $this->assertFileDoesNotExist($targetPath . 'target'); + } + + public function testMirrorAvoidCopyingTargetDirectoryIfInSourceDirectory() { + $sourcePath = $this->workspace . \DIRECTORY_SEPARATOR . 'source' . \DIRECTORY_SEPARATOR; + $directory = $sourcePath . 'directory' . \DIRECTORY_SEPARATOR; + $file1 = $directory . 'file1'; + $file2 = $sourcePath . 'file2'; + + mkdir($sourcePath); + mkdir($directory); + file_put_contents($file1, 'FILE1'); + file_put_contents($file2, 'FILE2'); + + $targetPath = $sourcePath . 'target' . \DIRECTORY_SEPARATOR; + + if ('\\' !== \DIRECTORY_SEPARATOR) { + Fs::symlink($targetPath, $sourcePath . 'target_simlink'); + } + + Fs::mirror($sourcePath, $targetPath, null, ['delete' => true]); + + $this->assertTrue(Fs::exists($targetPath)); + $this->assertTrue(Fs::exists($targetPath . 'directory')); + + $this->assertFileEquals($file1, $targetPath . 'directory' . \DIRECTORY_SEPARATOR . 'file1'); + $this->assertFileEquals($file2, $targetPath . 'file2'); + + $this->assertFalse(Fs::exists($targetPath . 'target_simlink')); + $this->assertFalse(Fs::exists($targetPath . 'target')); + } + + public function testMirrorFromSubdirectoryInToParentDirectory() { + $targetPath = $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR; + $sourcePath = $targetPath . 'bar' . \DIRECTORY_SEPARATOR; + $file1 = $sourcePath . 'file1'; + $file2 = $sourcePath . 'file2'; + + Fs::mkdir($sourcePath); + file_put_contents($file1, 'FILE1'); + file_put_contents($file2, 'FILE2'); + + Fs::mirror($sourcePath, $targetPath); + + $this->assertFileEquals($file1, $targetPath . 'file1'); + } + + /** + * @dataProvider providePathsForIsAbsolutePath + */ + public function testIsAbsolutePath($path, $expectedResult) { + $result = Fs::isAbsolutePath($path); + + $this->assertEquals($expectedResult, $result); + } + + public static function providePathsForIsAbsolutePath() { + return [ + ['/var/lib', true], + ['c:\\\\var\\lib', true], + ['\\var\\lib', true], + ['var/lib', false], + ['../var/lib', false], + ['', false], + ]; + } + + public function testTempnam() { + $dirname = $this->workspace; + + $filename = Fs::tempnam($dirname, 'foo'); + + $this->assertFileExists($filename); + } + + public function testTempnamWithFileScheme() { + $scheme = 'file://'; + $dirname = $scheme . $this->workspace; + + $filename = Fs::tempnam($dirname, 'foo'); + + $this->assertStringStartsWith($scheme, $filename); + $this->assertFileExists($filename); + } + + public function testTempnamWithMockScheme() { + stream_wrapper_register('mock', 'MensBeam\Filesystem\Test\MockStream'); + + $scheme = 'mock://'; + $dirname = $scheme . $this->workspace; + + $filename = Fs::tempnam($dirname, 'foo'); + + $this->assertStringStartsWith($scheme, $filename); + $this->assertFileExists($filename); + } + + public function testTempnamWithZlibSchemeFails() { + $this->expectException(IOException::class); + $scheme = 'compress.zlib://'; + $dirname = $scheme . $this->workspace; + + // The compress.zlib:// stream does not support mode x: creates the file, errors "failed to open stream: operation failed" and returns false + Fs::tempnam($dirname, 'bar'); + } + + public function testTempnamWithPHPTempSchemeFails() { + $scheme = 'php://temp'; + $dirname = $scheme; + + $filename = Fs::tempnam($dirname, 'bar'); + + $this->assertStringStartsWith($scheme, $filename); + + // The php://temp stream deletes the file after close + $this->assertFileDoesNotExist($filename); + } + + public function testTempnamWithPharSchemeFails() { + $this->expectException(IOException::class); + // Skip test if Phar disabled phar.readonly must be 0 in php.ini + if (!\Phar::canWrite()) { + $this->markTestSkipped('This test cannot run when phar.readonly is 1.'); + } + + $scheme = 'phar://'; + $dirname = $scheme . $this->workspace; + $pharname = 'foo.phar'; + + new \Phar($this->workspace . '/' . $pharname, 0, $pharname); + // The phar:// stream does not support mode x: fails to create file, errors "failed to open stream: phar error: "$filename" is not a file in phar "$pharname"" and returns false + Fs::tempnam($dirname, $pharname . '/bar'); + } + + public function testTempnamWithHTTPSchemeFails() { + $this->expectException(IOException::class); + $scheme = 'http://'; + $dirname = $scheme . $this->workspace; + + // The http:// scheme is read-only + Fs::tempnam($dirname, 'bar'); + } + + public function testTempnamOnUnwritableFallsBackToSysTmp() { + $scheme = 'file://'; + $dirname = $scheme . $this->workspace . \DIRECTORY_SEPARATOR . 'does_not_exist'; + + $filename = Fs::tempnam($dirname, 'bar'); + $realTempDir = realpath(sys_get_temp_dir()); + $this->assertStringStartsWith(rtrim($scheme . $realTempDir, \DIRECTORY_SEPARATOR), $filename); + $this->assertFileExists($filename); + + // Tear down + @unlink($filename); + } + + public function testTempnamWithSuffix() { + $dirname = $this->workspace; + $filename = Fs::tempnam($dirname, 'foo', '.bar'); + $this->assertStringEndsWith('.bar', $filename); + $this->assertFileExists($filename); + } + + public function testTempnamWithSuffix0() { + $dirname = $this->workspace; + $filename = Fs::tempnam($dirname, 'foo', '0'); + $this->assertStringEndsWith('0', $filename); + $this->assertFileExists($filename); + } + + public function testDumpFile() { + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'baz.txt'; + + // skip mode check on Windows + if ('\\' !== \DIRECTORY_SEPARATOR) { + $oldMask = umask(0002); + } + + Fs::dumpFile($filename, 'bar'); + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'bar'); + + // skip mode check on Windows + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->assertFilePermissions(664, $filename); + umask($oldMask); + } + } + + public function testDumpFileWithResource() { + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'baz.txt'; + + $resource = fopen('php://memory', 'rw'); + fwrite($resource, 'bar'); + fseek($resource, 0); + + Fs::dumpFile($filename, $resource); + + fclose($resource); + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'bar'); + } + + public function testDumpFileOverwritesAnExistingFile() { + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo.txt'; + file_put_contents($filename, 'FOO BAR'); + + Fs::dumpFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'bar'); + } + + public function testDumpFileFollowsSymlink() { + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo.txt'; + file_put_contents($filename, 'FOO BAR'); + $linknameA = $this->workspace . \DIRECTORY_SEPARATOR . 'bar.txt'; + $linknameB = $this->workspace . \DIRECTORY_SEPARATOR . 'baz.txt'; + Fs::symlink($filename, $linknameA); + Fs::symlink($linknameA, $linknameB); + + Fs::dumpFile($linknameB, 'bar'); + + $this->assertFileExists($filename); + $this->assertFileExists($linknameA); + $this->assertFileExists($linknameB); + $this->assertStringEqualsFile($filename, 'bar'); + $this->assertStringEqualsFile($linknameA, 'bar'); + $this->assertStringEqualsFile($linknameB, 'bar'); + + Fs::remove($filename); + Fs::dumpFile($linknameB, 'baz'); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'baz'); + $this->assertStringEqualsFile($linknameA, 'baz'); + $this->assertStringEqualsFile($linknameB, 'baz'); + } + + public function testDumpFileWithFileScheme() { + $scheme = 'file://'; + $filename = $scheme . $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'baz.txt'; + + Fs::dumpFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'bar'); + } + + public function testDumpFileWithZlibScheme() { + $scheme = 'compress.zlib://'; + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'baz.txt'; + + Fs::dumpFile($filename, 'bar'); + + // Zlib stat uses file:// wrapper so remove scheme + $this->assertFileExists(str_replace($scheme, '', $filename)); + $this->assertStringEqualsFile($filename, 'bar'); + } + + public function testAppendToFile() { + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'bar.txt'; + + // skip mode check on Windows + if ('\\' !== \DIRECTORY_SEPARATOR) { + $oldMask = umask(0002); + } + + Fs::dumpFile($filename, 'foo'); + + Fs::appendToFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'foobar'); + + // skip mode check on Windows + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->assertFilePermissions(664, $filename); + umask($oldMask); + } + } + + public function testAppendToFileWithResource() { + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'bar.txt'; + + // skip mode check on Windows + if ('\\' !== \DIRECTORY_SEPARATOR) { + $oldMask = umask(0002); + } + + Fs::dumpFile($filename, 'foo'); + + $resource = fopen('php://memory', 'rw'); + fwrite($resource, 'bar'); + fseek($resource, 0); + + Fs::appendToFile($filename, $resource); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'foobar'); + + // skip mode check on Windows + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->assertFilePermissions(664, $filename); + umask($oldMask); + } + } + + public function testAppendToFileFollowsSymlink() { + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo.txt'; + file_put_contents($filename, 'foo'); + $linknameA = $this->workspace . \DIRECTORY_SEPARATOR . 'bar.txt'; + $linknameB = $this->workspace . \DIRECTORY_SEPARATOR . 'baz.txt'; + Fs::symlink($filename, $linknameA); + Fs::symlink($linknameA, $linknameB); + + Fs::appendToFile($linknameA, 'bar'); + Fs::appendToFile($linknameB, 'baz'); + + $this->assertFileExists($filename); + $this->assertFileExists($linknameA); + $this->assertFileExists($linknameB); + $this->assertStringEqualsFile($filename, 'foobarbaz'); + $this->assertStringEqualsFile($linknameA, 'foobarbaz'); + $this->assertStringEqualsFile($linknameB, 'foobarbaz'); + + Fs::remove($filename); + Fs::appendToFile($linknameB, 'foo'); + Fs::appendToFile($linknameA, 'bar'); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'foobar'); + $this->assertStringEqualsFile($linknameA, 'foobar'); + $this->assertStringEqualsFile($linknameB, 'foobar'); + } + + public function testAppendToFileWithScheme() { + $scheme = 'file://'; + $filename = $scheme . $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'baz.txt'; + Fs::dumpFile($filename, 'foo'); + + Fs::appendToFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'foobar'); + } + + public function testAppendToFileWithZlibScheme() { + $scheme = 'compress.zlib://'; + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'baz.txt'; + Fs::dumpFile($filename, 'foo'); + + // Zlib stat uses file:// wrapper so remove it + $this->assertStringEqualsFile(str_replace($scheme, '', $filename), 'foo'); + + Fs::appendToFile($filename, 'bar'); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'foobar'); + } + + public function testAppendToFileCreateTheFileIfNotExists() { + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo' . \DIRECTORY_SEPARATOR . 'bar.txt'; + + // skip mode check on Windows + if ('\\' !== \DIRECTORY_SEPARATOR) { + $oldMask = umask(0002); + } + + Fs::appendToFile($filename, 'bar'); + + // skip mode check on Windows + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->assertFilePermissions(664, $filename); + umask($oldMask); + } + + $this->assertFileExists($filename); + $this->assertStringEqualsFile($filename, 'bar'); + } + + public function testDumpRemovesTmpFilesOnFailure() { + $parent = dirname(__DIR__); + $expected = scandir($parent, \SCANDIR_SORT_ASCENDING); + + try { + Fs::dumpFile($parent . '/lib', 'bar'); + $this->fail('IOException expected.'); + } catch (IOException $e) { + $this->assertSame($expected, scandir($parent, \SCANDIR_SORT_ASCENDING)); + } + } + + public function testDumpKeepsExistingPermissionsWhenOverwritingAnExistingFile() { + $this->markAsSkippedIfChmodIsMissing(); + + $filename = $this->workspace . \DIRECTORY_SEPARATOR . 'foo.txt'; + file_put_contents($filename, 'FOO BAR'); + chmod($filename, 0745); + + Fs::dumpFile($filename, 'bar'); + + $this->assertFilePermissions(745, $filename); + } + + public function testCopyShouldKeepExecutionPermission() { + $this->markAsSkippedIfChmodIsMissing(); + + $sourceFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_source_file'; + $targetFilePath = $this->workspace . \DIRECTORY_SEPARATOR . 'copy_target_file'; + + file_put_contents($sourceFilePath, 'SOURCE FILE'); + chmod($sourceFilePath, 0745); + + Fs::copy($sourceFilePath, $targetFilePath); + + $this->assertFilePermissions(767, $targetFilePath); + } + + public function testDumpToProtectedDirectory() { + if (\DIRECTORY_SEPARATOR !== '\\') { + $this->markTestSkipped('This test is specific to Windows.'); + } + + if (false === ($userProfilePath = getenv('USERPROFILE')) || !is_dir($userProfilePath)) { + throw new \RuntimeException('Failed to retrieve user profile path.'); + } + + $targetPath = implode(\DIRECTORY_SEPARATOR, [$userProfilePath, 'Downloads', '__test_file.ext']); + + try { + $this->assertFileDoesNotExist($targetPath); + Fs::dumpFile($targetPath, 'foobar'); + $this->assertFileExists($targetPath); + } finally { + Fs::remove($targetPath); + } + } + + /** + * Normalize the given path (transform each forward slash into a real directory separator). + */ + private function normalize(string $path): string { + return str_replace('/', \DIRECTORY_SEPARATOR, $path); + } +} diff --git a/tests/cases/TestPath.php b/tests/cases/TestPath.php new file mode 100644 index 0000000..ae4a1b9 --- /dev/null +++ b/tests/cases/TestPath.php @@ -0,0 +1,1010 @@ + + * @author Thomas Schulz + * @author Théo Fidry + * @covers \MensBeam\Path + */ +class TestPath extends TestCase { + protected array $storedEnv = []; + + protected function setUp(): void { + $this->storedEnv['HOME'] = getenv('HOME'); + $this->storedEnv['HOMEDRIVE'] = getenv('HOMEDRIVE'); + $this->storedEnv['HOMEPATH'] = getenv('HOMEPATH'); + + putenv('HOME=/home/webmozart'); + putenv('HOMEDRIVE='); + putenv('HOMEPATH='); + } + + protected function tearDown(): void { + putenv('HOME=' . $this->storedEnv['HOME']); + putenv('HOMEDRIVE=' . $this->storedEnv['HOMEDRIVE']); + putenv('HOMEPATH=' . $this->storedEnv['HOMEPATH']); + } + + public static function provideCanonicalizationTests(): \Generator { + // relative paths (forward slash) + yield ['css/./style.css', 'css/style.css']; + yield ['css/../style.css', 'style.css']; + yield ['css/./../style.css', 'style.css']; + yield ['css/.././style.css', 'style.css']; + yield ['css/../../style.css', '../style.css']; + yield ['./css/style.css', 'css/style.css']; + yield ['../css/style.css', '../css/style.css']; + yield ['./../css/style.css', '../css/style.css']; + yield ['.././css/style.css', '../css/style.css']; + yield ['../../css/style.css', '../../css/style.css']; + yield ['', '']; + yield ['.', '']; + yield ['..', '..']; + yield ['./..', '..']; + yield ['../.', '..']; + yield ['../..', '../..']; + + // relative paths (backslash) + yield ['css\\.\\style.css', 'css/style.css']; + yield ['css\\..\\style.css', 'style.css']; + yield ['css\\.\\..\\style.css', 'style.css']; + yield ['css\\..\\.\\style.css', 'style.css']; + yield ['css\\..\\..\\style.css', '../style.css']; + yield ['.\\css\\style.css', 'css/style.css']; + yield ['..\\css\\style.css', '../css/style.css']; + yield ['.\\..\\css\\style.css', '../css/style.css']; + yield ['..\\.\\css\\style.css', '../css/style.css']; + yield ['..\\..\\css\\style.css', '../../css/style.css']; + + // absolute paths (forward slash, UNIX) + yield ['/css/style.css', '/css/style.css']; + yield ['/css/./style.css', '/css/style.css']; + yield ['/css/../style.css', '/style.css']; + yield ['/css/./../style.css', '/style.css']; + yield ['/css/.././style.css', '/style.css']; + yield ['/./css/style.css', '/css/style.css']; + yield ['/../css/style.css', '/css/style.css']; + yield ['/./../css/style.css', '/css/style.css']; + yield ['/.././css/style.css', '/css/style.css']; + yield ['/../../css/style.css', '/css/style.css']; + + // absolute paths (backslash, UNIX) + yield ['\\css\\style.css', '/css/style.css']; + yield ['\\css\\.\\style.css', '/css/style.css']; + yield ['\\css\\..\\style.css', '/style.css']; + yield ['\\css\\.\\..\\style.css', '/style.css']; + yield ['\\css\\..\\.\\style.css', '/style.css']; + yield ['\\.\\css\\style.css', '/css/style.css']; + yield ['\\..\\css\\style.css', '/css/style.css']; + yield ['\\.\\..\\css\\style.css', '/css/style.css']; + yield ['\\..\\.\\css\\style.css', '/css/style.css']; + yield ['\\..\\..\\css\\style.css', '/css/style.css']; + + // absolute paths (forward slash, Windows) + yield ['C:/css/style.css', 'C:/css/style.css']; + yield ['C:/css/./style.css', 'C:/css/style.css']; + yield ['C:/css/../style.css', 'C:/style.css']; + yield ['C:/css/./../style.css', 'C:/style.css']; + yield ['C:/css/.././style.css', 'C:/style.css']; + yield ['C:/./css/style.css', 'C:/css/style.css']; + yield ['C:/../css/style.css', 'C:/css/style.css']; + yield ['C:/./../css/style.css', 'C:/css/style.css']; + yield ['C:/.././css/style.css', 'C:/css/style.css']; + yield ['C:/../../css/style.css', 'C:/css/style.css']; + + // absolute paths (backslash, Windows) + yield ['C:\\css\\style.css', 'C:/css/style.css']; + yield ['C:\\css\\.\\style.css', 'C:/css/style.css']; + yield ['C:\\css\\..\\style.css', 'C:/style.css']; + yield ['C:\\css\\.\\..\\style.css', 'C:/style.css']; + yield ['C:\\css\\..\\.\\style.css', 'C:/style.css']; + yield ['C:\\.\\css\\style.css', 'C:/css/style.css']; + yield ['C:\\..\\css\\style.css', 'C:/css/style.css']; + yield ['C:\\.\\..\\css\\style.css', 'C:/css/style.css']; + yield ['C:\\..\\.\\css\\style.css', 'C:/css/style.css']; + yield ['C:\\..\\..\\css\\style.css', 'C:/css/style.css']; + + // Windows special case + yield ['C:', 'C:/']; + + // Don't change malformed path + yield ['C:css/style.css', 'C:css/style.css']; + + // absolute paths (stream, UNIX) + yield ['phar:///css/style.css', 'phar:///css/style.css']; + yield ['phar:///css/./style.css', 'phar:///css/style.css']; + yield ['phar:///css/../style.css', 'phar:///style.css']; + yield ['phar:///css/./../style.css', 'phar:///style.css']; + yield ['phar:///css/.././style.css', 'phar:///style.css']; + yield ['phar:///./css/style.css', 'phar:///css/style.css']; + yield ['phar:///../css/style.css', 'phar:///css/style.css']; + yield ['phar:///./../css/style.css', 'phar:///css/style.css']; + yield ['phar:///.././css/style.css', 'phar:///css/style.css']; + yield ['phar:///../../css/style.css', 'phar:///css/style.css']; + + // absolute paths (stream, Windows) + yield ['phar://C:/css/style.css', 'phar://C:/css/style.css']; + yield ['phar://C:/css/./style.css', 'phar://C:/css/style.css']; + yield ['phar://C:/css/../style.css', 'phar://C:/style.css']; + yield ['phar://C:/css/./../style.css', 'phar://C:/style.css']; + yield ['phar://C:/css/.././style.css', 'phar://C:/style.css']; + yield ['phar://C:/./css/style.css', 'phar://C:/css/style.css']; + yield ['phar://C:/../css/style.css', 'phar://C:/css/style.css']; + yield ['phar://C:/./../css/style.css', 'phar://C:/css/style.css']; + yield ['phar://C:/.././css/style.css', 'phar://C:/css/style.css']; + yield ['phar://C:/../../css/style.css', 'phar://C:/css/style.css']; + + // paths with "~" UNIX + yield ['~/css/style.css', '/home/webmozart/css/style.css']; + yield ['~/css/./style.css', '/home/webmozart/css/style.css']; + yield ['~/css/../style.css', '/home/webmozart/style.css']; + yield ['~/css/./../style.css', '/home/webmozart/style.css']; + yield ['~/css/.././style.css', '/home/webmozart/style.css']; + yield ['~/./css/style.css', '/home/webmozart/css/style.css']; + yield ['~/../css/style.css', '/home/css/style.css']; + yield ['~/./../css/style.css', '/home/css/style.css']; + yield ['~/.././css/style.css', '/home/css/style.css']; + yield ['~/../../css/style.css', '/css/style.css']; + } + + /** + * @dataProvider provideCanonicalizationTests + */ + public function testCanonicalize(string $path, string $canonicalized) { + $this->assertSame($canonicalized, Path::canonicalize($path)); + } + + public static function provideGetDirectoryTests(): \Generator { + yield ['/webmozart/symfony/style.css', '/webmozart/symfony']; + yield ['/webmozart/symfony', '/webmozart']; + yield ['/webmozart', '/']; + yield ['/', '/']; + yield ['', '']; + + yield ['\\webmozart\\symfony\\style.css', '/webmozart/symfony']; + yield ['\\webmozart\\symfony', '/webmozart']; + yield ['\\webmozart', '/']; + yield ['\\', '/']; + + yield ['C:/webmozart/symfony/style.css', 'C:/webmozart/symfony']; + yield ['C:/webmozart/symfony', 'C:/webmozart']; + yield ['C:/webmozart', 'C:/']; + yield ['C:/', 'C:/']; + yield ['C:', 'C:/']; + + yield ['C:\\webmozart\\symfony\\style.css', 'C:/webmozart/symfony']; + yield ['C:\\webmozart\\symfony', 'C:/webmozart']; + yield ['C:\\webmozart', 'C:/']; + yield ['C:\\', 'C:/']; + + yield ['phar:///webmozart/symfony/style.css', 'phar:///webmozart/symfony']; + yield ['phar:///webmozart/symfony', 'phar:///webmozart']; + yield ['phar:///webmozart', 'phar:///']; + yield ['phar:///', 'phar:///']; + + yield ['phar://C:/webmozart/symfony/style.css', 'phar://C:/webmozart/symfony']; + yield ['phar://C:/webmozart/symfony', 'phar://C:/webmozart']; + yield ['phar://C:/webmozart', 'phar://C:/']; + yield ['phar://C:/', 'phar://C:/']; + + yield ['webmozart/symfony/style.css', 'webmozart/symfony']; + yield ['webmozart/symfony', 'webmozart']; + yield ['webmozart', '']; + + yield ['webmozart\\symfony\\style.css', 'webmozart/symfony']; + yield ['webmozart\\symfony', 'webmozart']; + yield ['webmozart', '']; + + yield ['/webmozart/./symfony/style.css', '/webmozart/symfony']; + yield ['/webmozart/../symfony/style.css', '/symfony']; + yield ['/webmozart/./../symfony/style.css', '/symfony']; + yield ['/webmozart/.././symfony/style.css', '/symfony']; + yield ['/webmozart/../../symfony/style.css', '/symfony']; + yield ['/.', '/']; + yield ['/..', '/']; + + yield ['C:webmozart', '']; + + yield ['D:/Folder/Aééé/Subfolder', 'D:/Folder/Aééé']; + } + + /** + * @dataProvider provideGetDirectoryTests + */ + public function testGetDirectory(string $path, string $directory) { + $this->assertSame($directory, Path::getDirectory($path)); + } + + public static function provideGetFilenameWithoutExtensionTests(): \Generator { + yield ['/webmozart/symfony/style.css.twig', null, 'style.css']; + yield ['/webmozart/symfony/style.css.', null, 'style.css']; + yield ['/webmozart/symfony/style.css', null, 'style']; + yield ['/webmozart/symfony/.style.css', null, '.style']; + yield ['/webmozart/symfony/', null, 'symfony']; + yield ['/webmozart/symfony', null, 'symfony']; + yield ['/', null, '']; + yield ['', null, '']; + + yield ['/webmozart/symfony/style.css', 'css', 'style']; + yield ['/webmozart/symfony/style.css', '.css', 'style']; + yield ['/webmozart/symfony/style.css', 'twig', 'style.css']; + yield ['/webmozart/symfony/style.css', '.twig', 'style.css']; + yield ['/webmozart/symfony/style.css', '', 'style.css']; + yield ['/webmozart/symfony/style.css.', '', 'style.css']; + yield ['/webmozart/symfony/style.css.', '.', 'style.css']; + yield ['/webmozart/symfony/style.css.', '.css', 'style.css']; + yield ['/webmozart/symfony/.style.css', 'css', '.style']; + yield ['/webmozart/symfony/.style.css', '.css', '.style']; + } + + /** + * @dataProvider provideGetFilenameWithoutExtensionTests + */ + public function testGetFilenameWithoutExtension(string $path, ?string $extension, string $filename) { + $this->assertSame($filename, Path::getFilenameWithoutExtension($path, $extension)); + } + + public static function provideGetExtensionTests(): \Generator { + yield ['/webmozart/symfony/style.css.twig', false, 'twig']; + yield ['/webmozart/symfony/style.css', false, 'css']; + yield ['/webmozart/symfony/style.css.', false, '']; + yield ['/webmozart/symfony/', false, '']; + yield ['/webmozart/symfony', false, '']; + yield ['/', false, '']; + yield ['', false, '']; + + yield ['/webmozart/symfony/style.CSS', false, 'CSS']; + yield ['/webmozart/symfony/style.CSS', true, 'css']; + yield ['/webmozart/symfony/style.ÄÖÜ', false, 'ÄÖÜ']; + + yield ['/webmozart/symfony/style.ÄÖÜ', true, 'äöü']; + } + + /** + * @dataProvider provideGetExtensionTests + */ + public function testGetExtension(string $path, bool $forceLowerCase, string $extension) { + $this->assertSame($extension, Path::getExtension($path, $forceLowerCase)); + } + + public static function provideHasExtensionTests(): \Generator { + yield [true, '/webmozart/symfony/style.css.twig', null, false]; + yield [true, '/webmozart/symfony/style.css', null, false]; + yield [false, '/webmozart/symfony/style.css.', null, false]; + yield [false, '/webmozart/symfony/', null, false]; + yield [false, '/webmozart/symfony', null, false]; + yield [false, '/', null, false]; + yield [false, '', null, false]; + + yield [true, '/webmozart/symfony/style.css.twig', 'twig', false]; + yield [false, '/webmozart/symfony/style.css.twig', 'css', false]; + yield [true, '/webmozart/symfony/style.css', 'css', false]; + yield [true, '/webmozart/symfony/style.css', '.css', false]; + yield [true, '/webmozart/symfony/style.css.', '', false]; + yield [false, '/webmozart/symfony/', 'ext', false]; + yield [false, '/webmozart/symfony', 'ext', false]; + yield [false, '/', 'ext', false]; + yield [false, '', 'ext', false]; + + yield [false, '/webmozart/symfony/style.css', 'CSS', false]; + yield [true, '/webmozart/symfony/style.css', 'CSS', true]; + yield [false, '/webmozart/symfony/style.CSS', 'css', false]; + yield [true, '/webmozart/symfony/style.CSS', 'css', true]; + yield [true, '/webmozart/symfony/style.ÄÖÜ', 'ÄÖÜ', false]; + + yield [true, '/webmozart/symfony/style.css', ['ext', 'css'], false]; + yield [true, '/webmozart/symfony/style.css', ['.ext', '.css'], false]; + yield [true, '/webmozart/symfony/style.css.', ['ext', ''], false]; + yield [false, '/webmozart/symfony/style.css', ['foo', 'bar', ''], false]; + yield [false, '/webmozart/symfony/style.css', ['.foo', '.bar', ''], false]; + + // This can only be tested, when mbstring is installed + yield [true, '/webmozart/symfony/style.ÄÖÜ', 'äöü', true]; + yield [true, '/webmozart/symfony/style.ÄÖÜ', ['äöü'], true]; + } + + /** + * @dataProvider provideHasExtensionTests + * + * @param string|string[]|null $extension + */ + public function testHasExtension(bool $hasExtension, string $path, $extension, bool $ignoreCase) { + $this->assertSame($hasExtension, Path::hasExtension($path, $extension, $ignoreCase)); + } + + public static function provideChangeExtensionTests(): \Generator { + yield ['/webmozart/symfony/style.css.twig', 'html', '/webmozart/symfony/style.css.html']; + yield ['/webmozart/symfony/style.css', 'sass', '/webmozart/symfony/style.sass']; + yield ['/webmozart/symfony/style.css', '.sass', '/webmozart/symfony/style.sass']; + yield ['/webmozart/symfony/style.css', '', '/webmozart/symfony/style.']; + yield ['/webmozart/symfony/style.css.', 'twig', '/webmozart/symfony/style.css.twig']; + yield ['/webmozart/symfony/style.css.', '', '/webmozart/symfony/style.css.']; + yield ['/webmozart/symfony/style.css', 'äöü', '/webmozart/symfony/style.äöü']; + yield ['/webmozart/symfony/style.äöü', 'css', '/webmozart/symfony/style.css']; + yield ['/webmozart/symfony/', 'css', '/webmozart/symfony/']; + yield ['/webmozart/symfony', 'css', '/webmozart/symfony.css']; + yield ['/', 'css', '/']; + yield ['', 'css', '']; + } + + /** + * @dataProvider provideChangeExtensionTests + */ + public function testChangeExtension(string $path, string $extension, string $pathExpected) { + $this->assertSame($pathExpected, Path::changeExtension($path, $extension)); + } + + public static function provideIsAbsolutePathTests(): \Generator { + yield ['/css/style.css', true]; + yield ['/', true]; + yield ['css/style.css', false]; + yield ['', false]; + + yield ['\\css\\style.css', true]; + yield ['\\', true]; + yield ['css\\style.css', false]; + + yield ['C:/css/style.css', true]; + yield ['D:/', true]; + + yield ['E:\\css\\style.css', true]; + yield ['F:\\', true]; + + yield ['phar:///css/style.css', true]; + yield ['phar:///', true]; + + // Windows special case + yield ['C:', true]; + + // Not considered absolute + yield ['C:css/style.css', false]; + } + + /** + * @dataProvider provideIsAbsolutePathTests + */ + public function testIsAbsolute(string $path, bool $isAbsolute) { + $this->assertSame($isAbsolute, Path::isAbsolute($path)); + } + + /** + * @dataProvider provideIsAbsolutePathTests + */ + public function testIsRelative(string $path, bool $isAbsolute) { + $this->assertSame(!$isAbsolute, Path::isRelative($path)); + } + + public static function provideGetRootTests(): \Generator { + yield ['/css/style.css', '/']; + yield ['/', '/']; + yield ['css/style.css', '']; + yield ['', '']; + + yield ['\\css\\style.css', '/']; + yield ['\\', '/']; + yield ['css\\style.css', '']; + + yield ['C:/css/style.css', 'C:/']; + yield ['C:/', 'C:/']; + yield ['C:', 'C:/']; + + yield ['D:\\css\\style.css', 'D:/']; + yield ['D:\\', 'D:/']; + + yield ['phar:///css/style.css', 'phar:///']; + yield ['phar:///', 'phar:///']; + + yield ['phar://C:/css/style.css', 'phar://C:/']; + yield ['phar://C:/', 'phar://C:/']; + yield ['phar://C:', 'phar://C:/']; + } + + /** + * @dataProvider provideGetRootTests + */ + public function testGetRoot(string $path, string $root) { + $this->assertSame($root, Path::getRoot($path)); + } + + private static function getPathTests(): \Generator { + yield from [ + // relative to absolute path + ['css/style.css', '/webmozart/symfony', '/webmozart/symfony/css/style.css'], + ['../css/style.css', '/webmozart/symfony', '/webmozart/css/style.css'], + ['../../css/style.css', '/webmozart/symfony', '/css/style.css'], + + // relative to root + ['css/style.css', '/', '/css/style.css'], + ['css/style.css', 'C:', 'C:/css/style.css'], + ['css/style.css', 'C:/', 'C:/css/style.css'], + + // same sub directories in different base directories + ['../../symfony/css/style.css', '/webmozart/css', '/symfony/css/style.css'], + + ['', '/webmozart/symfony', '/webmozart/symfony'], + ['..', '/webmozart/symfony', '/webmozart'], + ]; + } + + public static function provideMakeAbsoluteTests(): \Generator { + yield from self::getPathTests(); + + // collapse dots + yield ['css/./style.css', '/webmozart/symfony', '/webmozart/symfony/css/style.css']; + yield ['css/../style.css', '/webmozart/symfony', '/webmozart/symfony/style.css']; + yield ['css/./../style.css', '/webmozart/symfony', '/webmozart/symfony/style.css']; + yield ['css/.././style.css', '/webmozart/symfony', '/webmozart/symfony/style.css']; + yield ['./css/style.css', '/webmozart/symfony', '/webmozart/symfony/css/style.css']; + + yield ['css\\.\\style.css', '\\webmozart\\symfony', '/webmozart/symfony/css/style.css']; + yield ['css\\..\\style.css', '\\webmozart\\symfony', '/webmozart/symfony/style.css']; + yield ['css\\.\\..\\style.css', '\\webmozart\\symfony', '/webmozart/symfony/style.css']; + yield ['css\\..\\.\\style.css', '\\webmozart\\symfony', '/webmozart/symfony/style.css']; + yield ['.\\css\\style.css', '\\webmozart\\symfony', '/webmozart/symfony/css/style.css']; + + // collapse dots on root + yield ['./css/style.css', '/', '/css/style.css']; + yield ['../css/style.css', '/', '/css/style.css']; + yield ['../css/./style.css', '/', '/css/style.css']; + yield ['../css/../style.css', '/', '/style.css']; + yield ['../css/./../style.css', '/', '/style.css']; + yield ['../css/.././style.css', '/', '/style.css']; + + yield ['.\\css\\style.css', '\\', '/css/style.css']; + yield ['..\\css\\style.css', '\\', '/css/style.css']; + yield ['..\\css\\.\\style.css', '\\', '/css/style.css']; + yield ['..\\css\\..\\style.css', '\\', '/style.css']; + yield ['..\\css\\.\\..\\style.css', '\\', '/style.css']; + yield ['..\\css\\..\\.\\style.css', '\\', '/style.css']; + + yield ['./css/style.css', 'C:/', 'C:/css/style.css']; + yield ['../css/style.css', 'C:/', 'C:/css/style.css']; + yield ['../css/./style.css', 'C:/', 'C:/css/style.css']; + yield ['../css/../style.css', 'C:/', 'C:/style.css']; + yield ['../css/./../style.css', 'C:/', 'C:/style.css']; + yield ['../css/.././style.css', 'C:/', 'C:/style.css']; + + yield ['.\\css\\style.css', 'C:\\', 'C:/css/style.css']; + yield ['..\\css\\style.css', 'C:\\', 'C:/css/style.css']; + yield ['..\\css\\.\\style.css', 'C:\\', 'C:/css/style.css']; + yield ['..\\css\\..\\style.css', 'C:\\', 'C:/style.css']; + yield ['..\\css\\.\\..\\style.css', 'C:\\', 'C:/style.css']; + yield ['..\\css\\..\\.\\style.css', 'C:\\', 'C:/style.css']; + + yield ['./css/style.css', 'phar:///', 'phar:///css/style.css']; + yield ['../css/style.css', 'phar:///', 'phar:///css/style.css']; + yield ['../css/./style.css', 'phar:///', 'phar:///css/style.css']; + yield ['../css/../style.css', 'phar:///', 'phar:///style.css']; + yield ['../css/./../style.css', 'phar:///', 'phar:///style.css']; + yield ['../css/.././style.css', 'phar:///', 'phar:///style.css']; + + yield ['./css/style.css', 'phar://C:/', 'phar://C:/css/style.css']; + yield ['../css/style.css', 'phar://C:/', 'phar://C:/css/style.css']; + yield ['../css/./style.css', 'phar://C:/', 'phar://C:/css/style.css']; + yield ['../css/../style.css', 'phar://C:/', 'phar://C:/style.css']; + yield ['../css/./../style.css', 'phar://C:/', 'phar://C:/style.css']; + yield ['../css/.././style.css', 'phar://C:/', 'phar://C:/style.css']; + + // absolute paths + yield ['/css/style.css', '/webmozart/symfony', '/css/style.css']; + yield ['\\css\\style.css', '/webmozart/symfony', '/css/style.css']; + yield ['C:/css/style.css', 'C:/webmozart/symfony', 'C:/css/style.css']; + yield ['D:\\css\\style.css', 'D:/webmozart/symfony', 'D:/css/style.css']; + } + + /** + * @dataProvider provideMakeAbsoluteTests + */ + public function testMakeAbsolute(string $relativePath, string $basePath, string $absolutePath) { + $this->assertSame($absolutePath, Path::makeAbsolute($relativePath, $basePath)); + } + + public function testMakeAbsoluteFailsIfBasePathNotAbsolute() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The base path "webmozart/symfony" is not an absolute path.'); + + Path::makeAbsolute('css/style.css', 'webmozart/symfony'); + } + + public function testMakeAbsoluteFailsIfBasePathEmpty() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The base path must be a non-empty string. Got: ""'); + + Path::makeAbsolute('css/style.css', ''); + } + + public static function provideAbsolutePathsWithDifferentRoots(): \Generator { + yield ['C:/css/style.css', '/webmozart/symfony']; + yield ['C:/css/style.css', '\\webmozart\\symfony']; + yield ['C:\\css\\style.css', '/webmozart/symfony']; + yield ['C:\\css\\style.css', '\\webmozart\\symfony']; + + yield ['/css/style.css', 'C:/webmozart/symfony']; + yield ['/css/style.css', 'C:\\webmozart\\symfony']; + yield ['\\css\\style.css', 'C:/webmozart/symfony']; + yield ['\\css\\style.css', 'C:\\webmozart\\symfony']; + + yield ['D:/css/style.css', 'C:/webmozart/symfony']; + yield ['D:/css/style.css', 'C:\\webmozart\\symfony']; + yield ['D:\\css\\style.css', 'C:/webmozart/symfony']; + yield ['D:\\css\\style.css', 'C:\\webmozart\\symfony']; + + yield ['phar:///css/style.css', '/webmozart/symfony']; + yield ['/css/style.css', 'phar:///webmozart/symfony']; + + yield ['phar://C:/css/style.css', 'C:/webmozart/symfony']; + yield ['phar://C:/css/style.css', 'C:\\webmozart\\symfony']; + yield ['phar://C:\\css\\style.css', 'C:/webmozart/symfony']; + yield ['phar://C:\\css\\style.css', 'C:\\webmozart\\symfony']; + } + + /** + * @dataProvider provideAbsolutePathsWithDifferentRoots + */ + public function testMakeAbsoluteDoesNotFailIfDifferentRoot(string $basePath, string $absolutePath) { + // If a path in partition D: is passed, but $basePath is in partition + // C:, the path should be returned unchanged + $this->assertSame(Path::canonicalize($absolutePath), Path::makeAbsolute($absolutePath, $basePath)); + } + + public static function provideMakeRelativeTests(): \Generator { + foreach (self::getPathTests() as $set) { + yield [$set[2], $set[1], $set[0]]; + } + + yield ['/webmozart/symfony/./css/style.css', '/webmozart/symfony', 'css/style.css']; + yield ['/webmozart/symfony/../css/style.css', '/webmozart/symfony', '../css/style.css']; + yield ['/webmozart/symfony/.././css/style.css', '/webmozart/symfony', '../css/style.css']; + yield ['/webmozart/symfony/./../css/style.css', '/webmozart/symfony', '../css/style.css']; + yield ['/webmozart/symfony/../../css/style.css', '/webmozart/symfony', '../../css/style.css']; + yield ['/webmozart/symfony/css/style.css', '/webmozart/./symfony', 'css/style.css']; + yield ['/webmozart/symfony/css/style.css', '/webmozart/../symfony', '../webmozart/symfony/css/style.css']; + yield ['/webmozart/symfony/css/style.css', '/webmozart/./../symfony', '../webmozart/symfony/css/style.css']; + yield ['/webmozart/symfony/css/style.css', '/webmozart/.././symfony', '../webmozart/symfony/css/style.css']; + yield ['/webmozart/symfony/css/style.css', '/webmozart/../../symfony', '../webmozart/symfony/css/style.css']; + + // first argument shorter than second + yield ['/css', '/webmozart/symfony', '../../css']; + + // second argument shorter than first + yield ['/webmozart/symfony', '/css', '../webmozart/symfony']; + + yield ['\\webmozart\\symfony\\css\\style.css', '\\webmozart\\symfony', 'css/style.css']; + yield ['\\webmozart\\css\\style.css', '\\webmozart\\symfony', '../css/style.css']; + yield ['\\css\\style.css', '\\webmozart\\symfony', '../../css/style.css']; + + yield ['C:/webmozart/symfony/css/style.css', 'C:/webmozart/symfony', 'css/style.css']; + yield ['C:/webmozart/css/style.css', 'C:/webmozart/symfony', '../css/style.css']; + yield ['C:/css/style.css', 'C:/webmozart/symfony', '../../css/style.css']; + + yield ['C:\\webmozart\\symfony\\css\\style.css', 'C:\\webmozart\\symfony', 'css/style.css']; + yield ['C:\\webmozart\\css\\style.css', 'C:\\webmozart\\symfony', '../css/style.css']; + yield ['C:\\css\\style.css', 'C:\\webmozart\\symfony', '../../css/style.css']; + + yield ['phar:///webmozart/symfony/css/style.css', 'phar:///webmozart/symfony', 'css/style.css']; + yield ['phar:///webmozart/css/style.css', 'phar:///webmozart/symfony', '../css/style.css']; + yield ['phar:///css/style.css', 'phar:///webmozart/symfony', '../../css/style.css']; + + yield ['phar://C:/webmozart/symfony/css/style.css', 'phar://C:/webmozart/symfony', 'css/style.css']; + yield ['phar://C:/webmozart/css/style.css', 'phar://C:/webmozart/symfony', '../css/style.css']; + yield ['phar://C:/css/style.css', 'phar://C:/webmozart/symfony', '../../css/style.css']; + + // already relative + already in root basepath + yield ['../style.css', '/', 'style.css']; + yield ['./style.css', '/', 'style.css']; + yield ['../../style.css', '/', 'style.css']; + yield ['..\\style.css', 'C:\\', 'style.css']; + yield ['.\\style.css', 'C:\\', 'style.css']; + yield ['..\\..\\style.css', 'C:\\', 'style.css']; + yield ['../style.css', 'C:/', 'style.css']; + yield ['./style.css', 'C:/', 'style.css']; + yield ['../../style.css', 'C:/', 'style.css']; + yield ['..\\style.css', '\\', 'style.css']; + yield ['.\\style.css', '\\', 'style.css']; + yield ['..\\..\\style.css', '\\', 'style.css']; + yield ['../style.css', 'phar:///', 'style.css']; + yield ['./style.css', 'phar:///', 'style.css']; + yield ['../../style.css', 'phar:///', 'style.css']; + yield ['..\\style.css', 'phar://C:\\', 'style.css']; + yield ['.\\style.css', 'phar://C:\\', 'style.css']; + yield ['..\\..\\style.css', 'phar://C:\\', 'style.css']; + + yield ['css/../style.css', '/', 'style.css']; + yield ['css/./style.css', '/', 'css/style.css']; + yield ['css\\..\\style.css', 'C:\\', 'style.css']; + yield ['css\\.\\style.css', 'C:\\', 'css/style.css']; + yield ['css/../style.css', 'C:/', 'style.css']; + yield ['css/./style.css', 'C:/', 'css/style.css']; + yield ['css\\..\\style.css', '\\', 'style.css']; + yield ['css\\.\\style.css', '\\', 'css/style.css']; + yield ['css/../style.css', 'phar:///', 'style.css']; + yield ['css/./style.css', 'phar:///', 'css/style.css']; + yield ['css\\..\\style.css', 'phar://C:\\', 'style.css']; + yield ['css\\.\\style.css', 'phar://C:\\', 'css/style.css']; + + // already relative + yield ['css/style.css', '/webmozart/symfony', 'css/style.css']; + yield ['css\\style.css', '\\webmozart\\symfony', 'css/style.css']; + + // both relative + yield ['css/style.css', 'webmozart/symfony', '../../css/style.css']; + yield ['css\\style.css', 'webmozart\\symfony', '../../css/style.css']; + + // relative to empty + yield ['css/style.css', '', 'css/style.css']; + yield ['css\\style.css', '', 'css/style.css']; + + // different slashes in path and base path + yield ['/webmozart/symfony/css/style.css', '\\webmozart\\symfony', 'css/style.css']; + yield ['\\webmozart\\symfony\\css\\style.css', '/webmozart/symfony', 'css/style.css']; + } + + /** + * @dataProvider provideMakeRelativeTests + */ + public function testMakeRelative(string $absolutePath, string $basePath, string $relativePath) { + $this->assertSame($relativePath, Path::makeRelative($absolutePath, $basePath)); + } + + public function testMakeRelativeFailsIfAbsolutePathAndBasePathNotAbsolute() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The absolute path "/webmozart/symfony/css/style.css" cannot be made relative to the relative path "webmozart/symfony". You should provide an absolute base path instead.'); + + Path::makeRelative('/webmozart/symfony/css/style.css', 'webmozart/symfony'); + } + + public function testMakeRelativeFailsIfAbsolutePathAndBasePathEmpty() { + $this->expectExceptionMessage('The absolute path "/webmozart/symfony/css/style.css" cannot be made relative to the relative path "". You should provide an absolute base path instead.'); + + Path::makeRelative('/webmozart/symfony/css/style.css', ''); + } + + /** + * @dataProvider provideAbsolutePathsWithDifferentRoots + */ + public function testMakeRelativeFailsIfDifferentRoot(string $absolutePath, string $basePath) { + $this->expectException(\InvalidArgumentException::class); + + Path::makeRelative($absolutePath, $basePath); + } + + public static function provideIsLocalTests(): \Generator { + yield ['/bg.png', true]; + yield ['bg.png', true]; + yield ['http://example.com/bg.png', false]; + yield ['http://example.com', false]; + yield ['', false]; + } + + /** + * @dataProvider provideIsLocalTests + */ + public function testIsLocal(string $path, bool $isLocal) { + $this->assertSame($isLocal, Path::isLocal($path)); + } + + public static function provideGetLongestCommonBasePathTests(): \Generator { + // same paths + yield [['/base/path', '/base/path'], '/base/path']; + yield [['C:/base/path', 'C:/base/path'], 'C:/base/path']; + yield [['C:\\base\\path', 'C:\\base\\path'], 'C:/base/path']; + yield [['C:/base/path', 'C:\\base\\path'], 'C:/base/path']; + yield [['phar:///base/path', 'phar:///base/path'], 'phar:///base/path']; + yield [['phar://C:/base/path', 'phar://C:/base/path'], 'phar://C:/base/path']; + + // trailing slash + yield [['/base/path/', '/base/path'], '/base/path']; + yield [['C:/base/path/', 'C:/base/path'], 'C:/base/path']; + yield [['C:\\base\\path\\', 'C:\\base\\path'], 'C:/base/path']; + yield [['C:/base/path/', 'C:\\base\\path'], 'C:/base/path']; + yield [['phar:///base/path/', 'phar:///base/path'], 'phar:///base/path']; + yield [['phar://C:/base/path/', 'phar://C:/base/path'], 'phar://C:/base/path']; + + yield [['/base/path', '/base/path/'], '/base/path']; + yield [['C:/base/path', 'C:/base/path/'], 'C:/base/path']; + yield [['C:\\base\\path', 'C:\\base\\path\\'], 'C:/base/path']; + yield [['C:/base/path', 'C:\\base\\path\\'], 'C:/base/path']; + yield [['phar:///base/path', 'phar:///base/path/'], 'phar:///base/path']; + yield [['phar://C:/base/path', 'phar://C:/base/path/'], 'phar://C:/base/path']; + + // first in second + yield [['/base/path/sub', '/base/path'], '/base/path']; + yield [['C:/base/path/sub', 'C:/base/path'], 'C:/base/path']; + yield [['C:\\base\\path\\sub', 'C:\\base\\path'], 'C:/base/path']; + yield [['C:/base/path/sub', 'C:\\base\\path'], 'C:/base/path']; + yield [['phar:///base/path/sub', 'phar:///base/path'], 'phar:///base/path']; + yield [['phar://C:/base/path/sub', 'phar://C:/base/path'], 'phar://C:/base/path']; + + // second in first + yield [['/base/path', '/base/path/sub'], '/base/path']; + yield [['C:/base/path', 'C:/base/path/sub'], 'C:/base/path']; + yield [['C:\\base\\path', 'C:\\base\\path\\sub'], 'C:/base/path']; + yield [['C:/base/path', 'C:\\base\\path\\sub'], 'C:/base/path']; + yield [['phar:///base/path', 'phar:///base/path/sub'], 'phar:///base/path']; + yield [['phar://C:/base/path', 'phar://C:/base/path/sub'], 'phar://C:/base/path']; + + // first is prefix + yield [['/base/path/di', '/base/path/dir'], '/base/path']; + yield [['C:/base/path/di', 'C:/base/path/dir'], 'C:/base/path']; + yield [['C:\\base\\path\\di', 'C:\\base\\path\\dir'], 'C:/base/path']; + yield [['C:/base/path/di', 'C:\\base\\path\\dir'], 'C:/base/path']; + yield [['phar:///base/path/di', 'phar:///base/path/dir'], 'phar:///base/path']; + yield [['phar://C:/base/path/di', 'phar://C:/base/path/dir'], 'phar://C:/base/path']; + + // second is prefix + yield [['/base/path/dir', '/base/path/di'], '/base/path']; + yield [['C:/base/path/dir', 'C:/base/path/di'], 'C:/base/path']; + yield [['C:\\base\\path\\dir', 'C:\\base\\path\\di'], 'C:/base/path']; + yield [['C:/base/path/dir', 'C:\\base\\path\\di'], 'C:/base/path']; + yield [['phar:///base/path/dir', 'phar:///base/path/di'], 'phar:///base/path']; + yield [['phar://C:/base/path/dir', 'phar://C:/base/path/di'], 'phar://C:/base/path']; + + // root is common base path + yield [['/first', '/second'], '/']; + yield [['C:/first', 'C:/second'], 'C:/']; + yield [['C:\\first', 'C:\\second'], 'C:/']; + yield [['C:/first', 'C:\\second'], 'C:/']; + yield [['phar:///first', 'phar:///second'], 'phar:///']; + yield [['phar://C:/first', 'phar://C:/second'], 'phar://C:/']; + + // windows vs unix + yield [['/base/path', 'C:/base/path'], null]; + yield [['C:/base/path', '/base/path'], null]; + yield [['/base/path', 'C:\\base\\path'], null]; + yield [['phar:///base/path', 'phar://C:/base/path'], null]; + + // different partitions + yield [['C:/base/path', 'D:/base/path'], null]; + yield [['C:/base/path', 'D:\\base\\path'], null]; + yield [['C:\\base\\path', 'D:\\base\\path'], null]; + yield [['phar://C:/base/path', 'phar://D:/base/path'], null]; + + // three paths + yield [['/base/path/foo', '/base/path', '/base/path/bar'], '/base/path']; + yield [['C:/base/path/foo', 'C:/base/path', 'C:/base/path/bar'], 'C:/base/path']; + yield [['C:\\base\\path\\foo', 'C:\\base\\path', 'C:\\base\\path\\bar'], 'C:/base/path']; + yield [['C:/base/path//foo', 'C:/base/path', 'C:\\base\\path\\bar'], 'C:/base/path']; + yield [['phar:///base/path/foo', 'phar:///base/path', 'phar:///base/path/bar'], 'phar:///base/path']; + yield [['phar://C:/base/path/foo', 'phar://C:/base/path', 'phar://C:/base/path/bar'], 'phar://C:/base/path']; + + // three paths with root + yield [['/base/path/foo', '/', '/base/path/bar'], '/']; + yield [['C:/base/path/foo', 'C:/', 'C:/base/path/bar'], 'C:/']; + yield [['C:\\base\\path\\foo', 'C:\\', 'C:\\base\\path\\bar'], 'C:/']; + yield [['C:/base/path//foo', 'C:/', 'C:\\base\\path\\bar'], 'C:/']; + yield [['phar:///base/path/foo', 'phar:///', 'phar:///base/path/bar'], 'phar:///']; + yield [['phar://C:/base/path/foo', 'phar://C:/', 'phar://C:/base/path/bar'], 'phar://C:/']; + + // three paths, different roots + yield [['/base/path/foo', 'C:/base/path', '/base/path/bar'], null]; + yield [['/base/path/foo', 'C:\\base\\path', '/base/path/bar'], null]; + yield [['C:/base/path/foo', 'D:/base/path', 'C:/base/path/bar'], null]; + yield [['C:\\base\\path\\foo', 'D:\\base\\path', 'C:\\base\\path\\bar'], null]; + yield [['C:/base/path//foo', 'D:/base/path', 'C:\\base\\path\\bar'], null]; + yield [['phar:///base/path/foo', 'phar://C:/base/path', 'phar:///base/path/bar'], null]; + yield [['phar://C:/base/path/foo', 'phar://D:/base/path', 'phar://C:/base/path/bar'], null]; + + // only one path + yield [['/base/path'], '/base/path']; + yield [['C:/base/path'], 'C:/base/path']; + yield [['C:\\base\\path'], 'C:/base/path']; + yield [['phar:///base/path'], 'phar:///base/path']; + yield [['phar://C:/base/path'], 'phar://C:/base/path']; + } + + /** + * @dataProvider provideGetLongestCommonBasePathTests + * + * @param string[] $paths + */ + public function testGetLongestCommonBasePath(array $paths, ?string $basePath) { + $this->assertSame($basePath, Path::getLongestCommonBasePath(...$paths)); + } + + public static function provideIsBasePathTests(): \Generator { + // same paths + yield ['/base/path', '/base/path', true]; + yield ['C:/base/path', 'C:/base/path', true]; + yield ['C:\\base\\path', 'C:\\base\\path', true]; + yield ['C:/base/path', 'C:\\base\\path', true]; + yield ['phar:///base/path', 'phar:///base/path', true]; + yield ['phar://C:/base/path', 'phar://C:/base/path', true]; + + // trailing slash + yield ['/base/path/', '/base/path', true]; + yield ['C:/base/path/', 'C:/base/path', true]; + yield ['C:\\base\\path\\', 'C:\\base\\path', true]; + yield ['C:/base/path/', 'C:\\base\\path', true]; + yield ['phar:///base/path/', 'phar:///base/path', true]; + yield ['phar://C:/base/path/', 'phar://C:/base/path', true]; + + yield ['/base/path', '/base/path/', true]; + yield ['C:/base/path', 'C:/base/path/', true]; + yield ['C:\\base\\path', 'C:\\base\\path\\', true]; + yield ['C:/base/path', 'C:\\base\\path\\', true]; + yield ['phar:///base/path', 'phar:///base/path/', true]; + yield ['phar://C:/base/path', 'phar://C:/base/path/', true]; + + // first in second + yield ['/base/path/sub', '/base/path', false]; + yield ['C:/base/path/sub', 'C:/base/path', false]; + yield ['C:\\base\\path\\sub', 'C:\\base\\path', false]; + yield ['C:/base/path/sub', 'C:\\base\\path', false]; + yield ['phar:///base/path/sub', 'phar:///base/path', false]; + yield ['phar://C:/base/path/sub', 'phar://C:/base/path', false]; + + // second in first + yield ['/base/path', '/base/path/sub', true]; + yield ['C:/base/path', 'C:/base/path/sub', true]; + yield ['C:\\base\\path', 'C:\\base\\path\\sub', true]; + yield ['C:/base/path', 'C:\\base\\path\\sub', true]; + yield ['phar:///base/path', 'phar:///base/path/sub', true]; + yield ['phar://C:/base/path', 'phar://C:/base/path/sub', true]; + + // first is prefix + yield ['/base/path/di', '/base/path/dir', false]; + yield ['C:/base/path/di', 'C:/base/path/dir', false]; + yield ['C:\\base\\path\\di', 'C:\\base\\path\\dir', false]; + yield ['C:/base/path/di', 'C:\\base\\path\\dir', false]; + yield ['phar:///base/path/di', 'phar:///base/path/dir', false]; + yield ['phar://C:/base/path/di', 'phar://C:/base/path/dir', false]; + + // second is prefix + yield ['/base/path/dir', '/base/path/di', false]; + yield ['C:/base/path/dir', 'C:/base/path/di', false]; + yield ['C:\\base\\path\\dir', 'C:\\base\\path\\di', false]; + yield ['C:/base/path/dir', 'C:\\base\\path\\di', false]; + yield ['phar:///base/path/dir', 'phar:///base/path/di', false]; + yield ['phar://C:/base/path/dir', 'phar://C:/base/path/di', false]; + + // root + yield ['/', '/second', true]; + yield ['C:/', 'C:/second', true]; + yield ['C:', 'C:/second', true]; + yield ['C:\\', 'C:\\second', true]; + yield ['C:/', 'C:\\second', true]; + yield ['phar:///', 'phar:///second', true]; + yield ['phar://C:/', 'phar://C:/second', true]; + + // windows vs unix + yield ['/base/path', 'C:/base/path', false]; + yield ['C:/base/path', '/base/path', false]; + yield ['/base/path', 'C:\\base\\path', false]; + yield ['/base/path', 'phar:///base/path', false]; + yield ['phar:///base/path', 'phar://C:/base/path', false]; + + // different partitions + yield ['C:/base/path', 'D:/base/path', false]; + yield ['C:/base/path', 'D:\\base\\path', false]; + yield ['C:\\base\\path', 'D:\\base\\path', false]; + yield ['C:/base/path', 'phar://C:/base/path', false]; + yield ['phar://C:/base/path', 'phar://D:/base/path', false]; + } + + /** + * @dataProvider provideIsBasePathTests + */ + public function testIsBasePath(string $path, string $ofPath, bool $result) { + $this->assertSame($result, Path::isBasePath($path, $ofPath)); + } + + public static function provideJoinTests(): \Generator { + yield [['', ''], '']; + yield [['/path/to/test', ''], '/path/to/test']; + yield [['/path/to//test', ''], '/path/to/test']; + yield [['', '/path/to/test'], '/path/to/test']; + yield [['', '/path/to//test'], '/path/to/test']; + + yield [['/path/to/test', 'subdir'], '/path/to/test/subdir']; + yield [['/path/to/test/', 'subdir'], '/path/to/test/subdir']; + yield [['/path/to/test', '/subdir'], '/path/to/test/subdir']; + yield [['/path/to/test/', '/subdir'], '/path/to/test/subdir']; + yield [['/path/to/test', './subdir'], '/path/to/test/subdir']; + yield [['/path/to/test/', './subdir'], '/path/to/test/subdir']; + yield [['/path/to/test/', '../parentdir'], '/path/to/parentdir']; + yield [['/path/to/test', '../parentdir'], '/path/to/parentdir']; + yield [['path/to/test/', '/subdir'], 'path/to/test/subdir']; + yield [['path/to/test', '/subdir'], 'path/to/test/subdir']; + yield [['../path/to/test', '/subdir'], '../path/to/test/subdir']; + yield [['path', '../../subdir'], '../subdir']; + yield [['/path', '../../subdir'], '/subdir']; + yield [['../path', '../../subdir'], '../../subdir']; + + yield [['/path/to/test', 'subdir', ''], '/path/to/test/subdir']; + yield [['/path/to/test', '/subdir', ''], '/path/to/test/subdir']; + yield [['/path/to/test/', 'subdir', ''], '/path/to/test/subdir']; + yield [['/path/to/test/', '/subdir', ''], '/path/to/test/subdir']; + + yield [['/path', ''], '/path']; + yield [['/path', 'to', '/test', ''], '/path/to/test']; + yield [['/path', '', '/test', ''], '/path/test']; + yield [['path', 'to', 'test', ''], 'path/to/test']; + yield [[], '']; + + yield [['base/path', 'to/test'], 'base/path/to/test']; + + yield [['C:\\path\\to\\test', 'subdir'], 'C:/path/to/test/subdir']; + yield [['C:\\path\\to\\test\\', 'subdir'], 'C:/path/to/test/subdir']; + yield [['C:\\path\\to\\test', '/subdir'], 'C:/path/to/test/subdir']; + yield [['C:\\path\\to\\test\\', '/subdir'], 'C:/path/to/test/subdir']; + + yield [['/', 'subdir'], '/subdir']; + yield [['/', '/subdir'], '/subdir']; + yield [['C:/', 'subdir'], 'C:/subdir']; + yield [['C:/', '/subdir'], 'C:/subdir']; + yield [['C:\\', 'subdir'], 'C:/subdir']; + yield [['C:\\', '/subdir'], 'C:/subdir']; + yield [['C:', 'subdir'], 'C:/subdir']; + yield [['C:', '/subdir'], 'C:/subdir']; + + yield [['phar://', '/path/to/test'], 'phar:///path/to/test']; + yield [['phar:///', '/path/to/test'], 'phar:///path/to/test']; + yield [['phar:///path/to/test', 'subdir'], 'phar:///path/to/test/subdir']; + yield [['phar:///path/to/test', 'subdir/'], 'phar:///path/to/test/subdir']; + yield [['phar:///path/to/test', '/subdir'], 'phar:///path/to/test/subdir']; + yield [['phar:///path/to/test/', 'subdir'], 'phar:///path/to/test/subdir']; + yield [['phar:///path/to/test/', '/subdir'], 'phar:///path/to/test/subdir']; + + yield [['phar://', 'C:/path/to/test'], 'phar://C:/path/to/test']; + yield [['phar://', 'C:\\path\\to\\test'], 'phar://C:/path/to/test']; + yield [['phar://C:/path/to/test', 'subdir'], 'phar://C:/path/to/test/subdir']; + yield [['phar://C:/path/to/test', 'subdir/'], 'phar://C:/path/to/test/subdir']; + yield [['phar://C:/path/to/test', '/subdir'], 'phar://C:/path/to/test/subdir']; + yield [['phar://C:/path/to/test/', 'subdir'], 'phar://C:/path/to/test/subdir']; + yield [['phar://C:/path/to/test/', '/subdir'], 'phar://C:/path/to/test/subdir']; + yield [['phar://C:', 'path/to/test'], 'phar://C:/path/to/test']; + yield [['phar://C:', '/path/to/test'], 'phar://C:/path/to/test']; + yield [['phar://C:/', 'path/to/test'], 'phar://C:/path/to/test']; + yield [['phar://C:/', '/path/to/test'], 'phar://C:/path/to/test']; + } + + /** + * @dataProvider provideJoinTests + */ + public function testJoin(array $paths, $result) { + $this->assertSame($result, Path::join(...$paths)); + } + + public function testJoinVarArgs() { + $this->assertSame('/path', Path::join('/path')); + $this->assertSame('/path/to', Path::join('/path', 'to')); + $this->assertSame('/path/to/test', Path::join('/path', 'to', '/test')); + $this->assertSame('/path/to/test/subdir', Path::join('/path', 'to', '/test', 'subdir/')); + } + + public function testGetHomeDirectoryFailsIfNotSupportedOperatingSystem() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Your environment or operating system isn\'t supported'); + + putenv('HOME='); + + Path::getHomeDirectory(); + } + + public function testGetHomeDirectoryForUnix() { + $this->assertEquals('/home/webmozart', Path::getHomeDirectory()); + } + + public function testGetHomeDirectoryForWindows() { + putenv('HOME='); + putenv('HOMEDRIVE=C:'); + putenv('HOMEPATH=/users/webmozart'); + + $this->assertEquals('C:/users/webmozart', Path::getHomeDirectory()); + } + + public function testNormalize() { + $this->assertSame('C:/Foo/Bar/test', Path::normalize('C:\\Foo\\Bar/test')); + } +} diff --git a/tests/lib/FilesystemTestCase.php b/tests/lib/FilesystemTestCase.php new file mode 100644 index 0000000..4772894 --- /dev/null +++ b/tests/lib/FilesystemTestCase.php @@ -0,0 +1,146 @@ +umask = umask(0); + $this->workspace = sys_get_temp_dir() . '/' . microtime(true) . '.' . mt_rand(); + mkdir($this->workspace, 0777, true); + $this->workspace = realpath($this->workspace); + } + + protected function tearDown(): void { + if (!empty($this->longPathNamesWindows)) { + foreach ($this->longPathNamesWindows as $path) { + exec('DEL ' . $path); + } + $this->longPathNamesWindows = []; + } + + Fs::remove($this->workspace); + umask($this->umask); + } + + /** + * @param int $expectedFilePerms Expected file permissions as three digits (i.e. 755) + * @param string $filePath + */ + protected function assertFilePermissions($expectedFilePerms, $filePath) { + $actualFilePerms = (int) substr(sprintf('%o', fileperms($filePath)), -3); + $this->assertEquals( + $expectedFilePerms, + $actualFilePerms, + sprintf('File permissions for %s must be %s. Actual %s', $filePath, $expectedFilePerms, $actualFilePerms) + ); + } + + protected function getFileOwnerId($filepath) { + $this->markAsSkippedIfPosixIsMissing(); + + $infos = stat($filepath); + + return $infos['uid']; + } + + protected function getFileOwner($filepath) { + $this->markAsSkippedIfPosixIsMissing(); + + return ($datas = posix_getpwuid($this->getFileOwnerId($filepath))) ? $datas['name'] : null; + } + + protected function getFileGroupId($filepath) { + $this->markAsSkippedIfPosixIsMissing(); + + $infos = stat($filepath); + + return $infos['gid']; + } + + protected function getFileGroup($filepath) { + $this->markAsSkippedIfPosixIsMissing(); + + if ($datas = posix_getgrgid($this->getFileGroupId($filepath))) { + return $datas['name']; + } + + $this->markTestSkipped('Unable to retrieve file group name'); + } + + protected function markAsSkippedIfLinkIsMissing() { + if (!\function_exists('link')) { + $this->markTestSkipped('link is not supported'); + } + + if ('\\' === \DIRECTORY_SEPARATOR && false === self::$linkOnWindows) { + $this->markTestSkipped('link requires "Create hard links" privilege on windows'); + } + } + + protected function markAsSkippedIfSymlinkIsMissing($relative = false) { + if ('\\' === \DIRECTORY_SEPARATOR && false === self::$symlinkOnWindows) { + $this->markTestSkipped('symlink requires "Create symbolic links" privilege on Windows'); + } + + // https://bugs.php.net/69473 + if ($relative && '\\' === \DIRECTORY_SEPARATOR && 1 === \PHP_ZTS) { + $this->markTestSkipped('symlink does not support relative paths on thread safe Windows PHP versions'); + } + } + + protected function markAsSkippedIfChmodIsMissing() { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('chmod is not supported on Windows'); + } + } + + protected function markAsSkippedIfPosixIsMissing() { + if (!\function_exists('posix_isatty')) { + $this->markTestSkipped('Function posix_isatty is required.'); + } + } +} diff --git a/tests/lib/MockStream.php b/tests/lib/MockStream.php new file mode 100644 index 0000000..b9d7c57 --- /dev/null +++ b/tests/lib/MockStream.php @@ -0,0 +1,38 @@ + + + + + ./cases + + + + + + ../lib + + +