diff options
Diffstat (limited to 'vendor/symfony/filesystem')
| -rw-r--r-- | vendor/symfony/filesystem/CHANGELOG.md | 82 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/Exception/ExceptionInterface.php | 21 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/Exception/FileNotFoundException.php | 34 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/Exception/IOException.php | 36 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/Exception/IOExceptionInterface.php | 25 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/Exception/InvalidArgumentException.php | 19 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/Exception/RuntimeException.php | 19 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/Filesystem.php | 782 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/LICENSE | 19 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/Path.php | 816 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/README.md | 13 | ||||
| -rw-r--r-- | vendor/symfony/filesystem/composer.json | 33 | 
12 files changed, 1899 insertions, 0 deletions
diff --git a/vendor/symfony/filesystem/CHANGELOG.md b/vendor/symfony/filesystem/CHANGELOG.md new file mode 100644 index 000000000..fcb7170ca --- /dev/null +++ b/vendor/symfony/filesystem/CHANGELOG.md @@ -0,0 +1,82 @@ +CHANGELOG +========= + +5.4 +--- + + * Add `Path` class + * Add `$lock` argument to `Filesystem::appendToFile()` + +5.0.0 +----- + + * `Filesystem::dumpFile()` and `appendToFile()` don't accept arrays anymore + +4.4.0 +----- + + * support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated and will be removed in 5.0 + * `tempnam()` now accepts a third argument `$suffix`. + +4.3.0 +----- + + * support for passing arrays to `Filesystem::dumpFile()` is deprecated and will be removed in 5.0 + * support for passing arrays to `Filesystem::appendToFile()` is deprecated and will be removed in 5.0 + +4.0.0 +----- + + * removed `LockHandler` + * Support for passing relative paths to `Filesystem::makePathRelative()` has been removed. + +3.4.0 +----- + + * support for passing relative paths to `Filesystem::makePathRelative()` is deprecated and will be removed in 4.0 + +3.3.0 +----- + + * added `appendToFile()` to append contents to existing files + +3.2.0 +----- + + * added `readlink()` as a platform independent method to read links + +3.0.0 +----- + + * removed `$mode` argument from `Filesystem::dumpFile()` + +2.8.0 +----- + + * added tempnam() a stream aware version of PHP's native tempnam() + +2.6.0 +----- + + * added LockHandler + +2.3.12 +------ + + * deprecated dumpFile() file mode argument. + +2.3.0 +----- + + * added the dumpFile() method to atomically write files + +2.2.0 +----- + + * added a delete option for the mirror() method + +2.1.0 +----- + + * 24eb396 : BC Break : mkdir() function now throws exception in case of failure instead of returning Boolean value + * created the component diff --git a/vendor/symfony/filesystem/Exception/ExceptionInterface.php b/vendor/symfony/filesystem/Exception/ExceptionInterface.php new file mode 100644 index 000000000..fc438d9f3 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Romain Neutron <imprec@gmail.com> + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/vendor/symfony/filesystem/Exception/FileNotFoundException.php b/vendor/symfony/filesystem/Exception/FileNotFoundException.php new file mode 100644 index 000000000..06b732b16 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/FileNotFoundException.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception class thrown when a file couldn't be found. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Christian Gärtner <christiangaertner.film@googlemail.com> + */ +class FileNotFoundException extends IOException +{ +    public function __construct(?string $message = null, int $code = 0, ?\Throwable $previous = null, ?string $path = null) +    { +        if (null === $message) { +            if (null === $path) { +                $message = 'File could not be found.'; +            } else { +                $message = sprintf('File "%s" could not be found.', $path); +            } +        } + +        parent::__construct($message, $code, $previous, $path); +    } +} diff --git a/vendor/symfony/filesystem/Exception/IOException.php b/vendor/symfony/filesystem/Exception/IOException.php new file mode 100644 index 000000000..df3a0850a --- /dev/null +++ b/vendor/symfony/filesystem/Exception/IOException.php @@ -0,0 +1,36 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception class thrown when a filesystem operation failure happens. + * + * @author Romain Neutron <imprec@gmail.com> + * @author Christian Gärtner <christiangaertner.film@googlemail.com> + * @author Fabien Potencier <fabien@symfony.com> + */ +class IOException extends \RuntimeException implements IOExceptionInterface +{ +    private ?string $path; + +    public function __construct(string $message, int $code = 0, ?\Throwable $previous = null, ?string $path = null) +    { +        $this->path = $path; + +        parent::__construct($message, $code, $previous); +    } + +    public function getPath(): ?string +    { +        return $this->path; +    } +} diff --git a/vendor/symfony/filesystem/Exception/IOExceptionInterface.php b/vendor/symfony/filesystem/Exception/IOExceptionInterface.php new file mode 100644 index 000000000..90c71db88 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/IOExceptionInterface.php @@ -0,0 +1,25 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * IOException interface for file and input/output stream related exceptions thrown by the component. + * + * @author Christian Gärtner <christiangaertner.film@googlemail.com> + */ +interface IOExceptionInterface extends ExceptionInterface +{ +    /** +     * Returns the associated path for the exception. +     */ +    public function getPath(): ?string; +} diff --git a/vendor/symfony/filesystem/Exception/InvalidArgumentException.php b/vendor/symfony/filesystem/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..abadc2002 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * @author Christian Flothmann <christian.flothmann@sensiolabs.de> + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/filesystem/Exception/RuntimeException.php b/vendor/symfony/filesystem/Exception/RuntimeException.php new file mode 100644 index 000000000..a7512dca7 --- /dev/null +++ b/vendor/symfony/filesystem/Exception/RuntimeException.php @@ -0,0 +1,19 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * @author Théo Fidry <theo.fidry@gmail.com> + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/vendor/symfony/filesystem/Filesystem.php b/vendor/symfony/filesystem/Filesystem.php new file mode 100644 index 000000000..d46aa4a42 --- /dev/null +++ b/vendor/symfony/filesystem/Filesystem.php @@ -0,0 +1,782 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem; + +use Symfony\Component\Filesystem\Exception\FileNotFoundException; +use Symfony\Component\Filesystem\Exception\InvalidArgumentException; +use Symfony\Component\Filesystem\Exception\IOException; + +/** + * Provides basic utility to manipulate the file system. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Filesystem +{ +    private static ?string $lastError = null; + +    /** +     * Copies a file. +     * +     * If the target file is older than the origin file, it's always overwritten. +     * If the target file is newer, it is overwritten only when the +     * $overwriteNewerFiles option is set to true. +     * +     * @return void +     * +     * @throws FileNotFoundException When originFile doesn't exist +     * @throws IOException           When copy fails +     */ +    public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false) +    { +        $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://'); +        if ($originIsLocal && !is_file($originFile)) { +            throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); +        } + +        $this->mkdir(\dirname($targetFile)); + +        $doCopy = true; +        if (!$overwriteNewerFiles && !parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) { +            $doCopy = filemtime($originFile) > filemtime($targetFile); +        } + +        if ($doCopy) { +            // https://bugs.php.net/64634 +            if (!$source = self::box('fopen', $originFile, 'r')) { +                throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile); +            } + +            // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default +            if (!$target = self::box('fopen', $targetFile, 'w', false, stream_context_create(['ftp' => ['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); +            } + +            $bytesCopied = stream_copy_to_stream($source, $target); +            fclose($source); +            fclose($target); +            unset($source, $target); + +            if (!is_file($targetFile)) { +                throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); +            } + +            if ($originIsLocal) { +                // Like `cp`, preserve executable permission bits +                self::box('chmod', $targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111)); + +                // Like `cp`, preserve the file modification time +                self::box('touch', $targetFile, filemtime($originFile)); + +                if ($bytesCopied !== $bytesOrigin = filesize($originFile)) { +                    throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile); +                } +            } +        } +    } + +    /** +     * Creates a directory recursively. +     * +     * @return void +     * +     * @throws IOException On any directory creation failure +     */ +    public function mkdir(string|iterable $dirs, int $mode = 0777) +    { +        foreach ($this->toIterable($dirs) as $dir) { +            if (is_dir($dir)) { +                continue; +            } + +            if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) { +                throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir); +            } +        } +    } + +    /** +     * Checks the existence of files or directories. +     */ +    public function exists(string|iterable $files): bool +    { +        $maxPathLength = \PHP_MAXPATHLEN - 2; + +        foreach ($this->toIterable($files) as $file) { +            if (\strlen($file) > $maxPathLength) { +                throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file); +            } + +            if (!file_exists($file)) { +                return false; +            } +        } + +        return true; +    } + +    /** +     * Sets access and modification time of file. +     * +     * @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 function touch(string|iterable $files, ?int $time = null, ?int $atime = null) +    { +        foreach ($this->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); +            } +        } +    } + +    /** +     * Removes files or directories. +     * +     * @return void +     * +     * @throws IOException When removal fails +     */ +    public function remove(string|iterable $files) +    { +        if ($files instanceof \Traversable) { +            $files = iterator_to_array($files, false); +        } elseif (!\is_array($files)) { +            $files = [$files]; +        } + +        self::doRemove($files, false); +    } + +    private static function doRemove(array $files, bool $isRecursive): void +    { +        $files = array_reverse($files); +        foreach ($files as $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); +                } +            } elseif (is_dir($file)) { +                if (!$isRecursive) { +                    $tmpName = \dirname(realpath($file)).'/.!'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-!')); + +                    if (file_exists($tmpName)) { +                        try { +                            self::doRemove([$tmpName], true); +                        } catch (IOException) { +                        } +                    } + +                    if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) { +                        $origFile = $file; +                        $file = $tmpName; +                    } else { +                        $origFile = null; +                    } +                } + +                $filesystemIterator = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS); +                self::doRemove(iterator_to_array($filesystemIterator, true), true); + +                if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) { +                    $lastError = self::$lastError; + +                    if (null !== $origFile && self::box('rename', $file, $origFile)) { +                        $file = $origFile; +                    } + +                    throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError); +                } +            } elseif (!self::box('unlink', $file) && ((self::$lastError && str_contains(self::$lastError, 'Permission denied')) || file_exists($file))) { +                throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError); +            } +        } +    } + +    /** +     * Change mode for an array of files or directories. +     * +     * @param int  $mode      The new mode (octal) +     * @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 function chmod(string|iterable $files, int $mode, int $umask = 0000, bool $recursive = false) +    { +        foreach ($this->toIterable($files) as $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)) { +                $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); +            } +        } +    } + +    /** +     * Change the owner of an array of files or directories. +     * +     * This method always throws on Windows, as the underlying PHP function is not supported. +     * +     * @see https://www.php.net/chown +     * +     * @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 function chown(string|iterable $files, string|int $user, bool $recursive = false) +    { +        foreach ($this->toIterable($files) as $file) { +            if ($recursive && is_dir($file) && !is_link($file)) { +                $this->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); +                } +            } else { +                if (!self::box('chown', $file, $user)) { +                    throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file); +                } +            } +        } +    } + +    /** +     * Change the group of an array of files or directories. +     * +     * This method always throws on Windows, as the underlying PHP function is not supported. +     * +     * @see https://www.php.net/chgrp +     * +     * @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 function chgrp(string|iterable $files, string|int $group, bool $recursive = false) +    { +        foreach ($this->toIterable($files) as $file) { +            if ($recursive && is_dir($file) && !is_link($file)) { +                $this->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); +                } +            } else { +                if (!self::box('chgrp', $file, $group)) { +                    throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file); +                } +            } +        } +    } + +    /** +     * Renames a file or a directory. +     * +     * @return void +     * +     * @throws IOException When target file or directory already exists +     * @throws IOException When origin cannot be renamed +     */ +    public function rename(string $origin, string $target, bool $overwrite = false) +    { +        // we check that target does not exist +        if (!$overwrite && $this->isReadable($target)) { +            throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); +        } + +        if (!self::box('rename', $origin, $target)) { +            if (is_dir($origin)) { +                // See https://bugs.php.net/54097 & https://php.net/rename#113943 +                $this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]); +                $this->remove($origin); + +                return; +            } +            throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target); +        } +    } + +    /** +     * Tells whether a file exists and is readable. +     * +     * @throws IOException When windows path is longer than 258 characters +     */ +    private function isReadable(string $filename): bool +    { +        $maxPathLength = \PHP_MAXPATHLEN - 2; + +        if (\strlen($filename) > $maxPathLength) { +            throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename); +        } + +        return is_readable($filename); +    } + +    /** +     * Creates a symbolic link or copy a directory. +     * +     * @return void +     * +     * @throws IOException When symlink fails +     */ +    public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false) +    { +        self::assertFunctionExists('symlink'); + +        if ('\\' === \DIRECTORY_SEPARATOR) { +            $originDir = strtr($originDir, '/', '\\'); +            $targetDir = strtr($targetDir, '/', '\\'); + +            if ($copyOnWindows) { +                $this->mirror($originDir, $targetDir); + +                return; +            } +        } + +        $this->mkdir(\dirname($targetDir)); + +        if (is_link($targetDir)) { +            if (readlink($targetDir) === $originDir) { +                return; +            } +            $this->remove($targetDir); +        } + +        if (!self::box('symlink', $originDir, $targetDir)) { +            $this->linkException($originDir, $targetDir, 'symbolic'); +        } +    } + +    /** +     * Creates a hard link, or several hard links to a file. +     * +     * @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 +     */ +    public function hardlink(string $originFile, string|iterable $targetFiles) +    { +        self::assertFunctionExists('link'); + +        if (!$this->exists($originFile)) { +            throw new FileNotFoundException(null, 0, null, $originFile); +        } + +        if (!is_file($originFile)) { +            throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile)); +        } + +        foreach ($this->toIterable($targetFiles) as $targetFile) { +            if (is_file($targetFile)) { +                if (fileinode($originFile) === fileinode($targetFile)) { +                    continue; +                } +                $this->remove($targetFile); +            } + +            if (!self::box('link', $originFile, $targetFile)) { +                $this->linkException($originFile, $targetFile, 'hard'); +            } +        } +    } + +    /** +     * @param string $linkType Name of the link type, typically 'symbolic' or 'hard' +     */ +    private 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); +    } + +    /** +     * Resolves links in paths. +     * +     * With $canonicalize = false (default) +     *      - if $path does not exist or is not a link, returns null +     *      - if $path is a link, returns the next direct target of the link without considering the existence of the target +     * +     * With $canonicalize = true +     *      - if $path does not exist, returns null +     *      - if $path exists, returns its absolute fully resolved final version +     */ +    public function readlink(string $path, bool $canonicalize = false): ?string +    { +        if (!$canonicalize && !is_link($path)) { +            return null; +        } + +        if ($canonicalize) { +            if (!$this->exists($path)) { +                return null; +            } + +            return realpath($path); +        } + +        return readlink($path); +    } + +    /** +     * Given an existing path, convert it to a path relative to a given starting path. +     */ +    public function makePathRelative(string $endPath, string $startPath): string +    { +        if (!$this->isAbsolutePath($startPath)) { +            throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.', $startPath)); +        } + +        if (!$this->isAbsolutePath($endPath)) { +            throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.', $endPath)); +        } + +        // Normalize separators on Windows +        if ('\\' === \DIRECTORY_SEPARATOR) { +            $endPath = str_replace('\\', '/', $endPath); +            $startPath = str_replace('\\', '/', $startPath); +        } + +        $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 = []; + +            foreach (explode('/', trim($path, '/')) as $segment) { +                if ('..' === $segment) { +                    array_pop($result); +                } elseif ('.' !== $segment && '' !== $segment) { +                    $result[] = $segment; +                } +            } + +            return $result; +        }; + +        [$endPath, $endDriveLetter] = $splitDriveLetter($endPath); +        [$startPath, $startDriveLetter] = $splitDriveLetter($startPath); + +        $startPathArr = $splitPath($startPath); +        $endPathArr = $splitPath($endPath); + +        if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) { +            // End path is on another drive, so no relative path exists +            return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : ''); +        } + +        // Find for which directory the common path stops +        $index = 0; +        while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) { +            ++$index; +        } + +        // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels) +        if (1 === \count($startPathArr) && '' === $startPathArr[0]) { +            $depth = 0; +        } else { +            $depth = \count($startPathArr) - $index; +        } + +        // Repeated "../" for each level need to reach the common path +        $traverser = str_repeat('../', $depth); + +        $endPathRemainder = implode('/', \array_slice($endPathArr, $index)); + +        // Construct $endPath from traversing to the common path, then to the remaining $endPath +        $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : ''); + +        return '' === $relativePath ? './' : $relativePath; +    } + +    /** +     * Mirrors a directory to another. +     * +     * Copies files and directories from the origin directory into the target directory. By default: +     * +     *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option) +     *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option) +     * +     * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created +     * @param array             $options  An array of boolean options +     *                                    Valid options are: +     *                                    - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false) +     *                                    - $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 function mirror(string $originDir, string $targetDir, ?\Traversable $iterator = null, array $options = []) +    { +        $targetDir = rtrim($targetDir, '/\\'); +        $originDir = rtrim($originDir, '/\\'); +        $originDirLen = \strlen($originDir); + +        if (!$this->exists($originDir)) { +            throw new IOException(sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir); +        } + +        // Iterate in destination folder to remove obsolete entries +        if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) { +            $deleteIterator = $iterator; +            if (null === $deleteIterator) { +                $flags = \FilesystemIterator::SKIP_DOTS; +                $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST); +            } +            $targetDirLen = \strlen($targetDir); +            foreach ($deleteIterator as $file) { +                $origin = $originDir.substr($file->getPathname(), $targetDirLen); +                if (!$this->exists($origin)) { +                    $this->remove($file); +                } +            } +        } + +        $copyOnWindows = $options['copy_on_windows'] ?? false; + +        if (null === $iterator) { +            $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS; +            $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST); +        } + +        $this->mkdir($targetDir); +        $filesCreatedWhileMirroring = []; + +        foreach ($iterator as $file) { +            if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) { +                continue; +            } + +            $target = $targetDir.substr($file->getPathname(), $originDirLen); +            $filesCreatedWhileMirroring[$target] = true; + +            if (!$copyOnWindows && is_link($file)) { +                $this->symlink($file->getLinkTarget(), $target); +            } elseif (is_dir($file)) { +                $this->mkdir($target); +            } elseif (is_file($file)) { +                $this->copy($file, $target, $options['override'] ?? false); +            } else { +                throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); +            } +        } +    } + +    /** +     * Returns whether the file path is an absolute path. +     */ +    public function isAbsolutePath(string $file): bool +    { +        return '' !== $file && (strspn($file, '/\\', 0, 1) +            || (\strlen($file) > 3 && ctype_alpha($file[0]) +                && ':' === $file[1] +                && strspn($file, '/\\', 2, 1) +            ) +            || null !== parse_url($file, \PHP_URL_SCHEME) +        ); +    } + +    /** +     * Creates a temporary file with support for custom stream wrappers. +     * +     * @param string $prefix The prefix of the generated temporary filename +     *                       Note: Windows uses only the first three characters of prefix +     * @param string $suffix The suffix of the generated temporary filename +     * +     * @return string The new temporary filename (with path), or throw an exception on failure +     */ +    public function tempnam(string $dir, string $prefix, string $suffix = ''): string +    { +        [$scheme, $hierarchy] = $this->getSchemeAndHierarchy($dir); + +        // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem +        if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) { +            // 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 $tmpFile; +            } + +            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(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 +            if (!$handle = self::box('fopen', $tmpFile, 'x+')) { +                continue; +            } + +            // Close the file if it was successfully opened +            self::box('fclose', $handle); + +            return $tmpFile; +        } + +        throw new IOException('A temporary file could not be created: '.self::$lastError); +    } + +    /** +     * Atomically dumps content into a file. +     * +     * @param string|resource $content The data to write into the file +     * +     * @return void +     * +     * @throws IOException if the file cannot be written to +     */ +    public function dumpFile(string $filename, $content) +    { +        if (\is_array($content)) { +            throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); +        } + +        $dir = \dirname($filename); + +        if (is_link($filename) && $linkTarget = $this->readlink($filename)) { +            $this->dumpFile(Path::makeAbsolute($linkTarget, $dir), $content); + +            return; +        } + +        if (!is_dir($dir)) { +            $this->mkdir($dir); +        } + +        // Will create a temp file with 0600 access rights +        // when the filesystem supports chmod. +        $tmpFile = $this->tempnam($dir, basename($filename)); + +        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); +            } + +            self::box('chmod', $tmpFile, self::box('fileperms', $filename) ?: 0666 & ~umask()); + +            $this->rename($tmpFile, $filename, true); +        } finally { +            if (file_exists($tmpFile)) { +                if ('\\' === \DIRECTORY_SEPARATOR && !is_writable($tmpFile)) { +                    self::box('chmod', $tmpFile, self::box('fileperms', $tmpFile) | 0200); +                } + +                self::box('unlink', $tmpFile); +            } +        } +    } + +    /** +     * Appends content to an existing file. +     * +     * @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 function appendToFile(string $filename, $content/* , bool $lock = false */) +    { +        if (\is_array($content)) { +            throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__)); +        } + +        $dir = \dirname($filename); + +        if (!is_dir($dir)) { +            $this->mkdir($dir); +        } + +        $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); +        } +    } + +    private function toIterable(string|iterable $files): iterable +    { +        return is_iterable($files) ? $files : [$files]; +    } + +    /** +     * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]). +     */ +    private function getSchemeAndHierarchy(string $filename): array +    { +        $components = explode('://', $filename, 2); + +        return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]]; +    } + +    private static function assertFunctionExists(string $func): void +    { +        if (!\function_exists($func)) { +            throw new IOException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func)); +        } +    } + +    private static function box(string $func, mixed ...$args): mixed +    { +        self::assertFunctionExists($func); + +        self::$lastError = null; +        set_error_handler(self::handleError(...)); +        try { +            return $func(...$args); +        } finally { +            restore_error_handler(); +        } +    } + +    /** +     * @internal +     */ +    public static function handleError(int $type, string $msg): void +    { +        self::$lastError = $msg; +    } +} diff --git a/vendor/symfony/filesystem/LICENSE b/vendor/symfony/filesystem/LICENSE new file mode 100644 index 000000000..0138f8f07 --- /dev/null +++ b/vendor/symfony/filesystem/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/filesystem/Path.php b/vendor/symfony/filesystem/Path.php new file mode 100644 index 000000000..948e1c41b --- /dev/null +++ b/vendor/symfony/filesystem/Path.php @@ -0,0 +1,816 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem; + +use Symfony\Component\Filesystem\Exception\InvalidArgumentException; +use Symfony\Component\Filesystem\Exception\RuntimeException; + +/** + * Contains utility methods for handling path strings. + * + * The methods in this class are able to deal with both UNIX and Windows paths + * with both forward and backward slashes. All methods return normalized parts + * containing only forward slashes and no excess "." and ".." segments. + * + * @author Bernhard Schussek <bschussek@gmail.com> + * @author Thomas Schulz <mail@king2500.net> + * @author Théo Fidry <theo.fidry@gmail.com> + */ +final class Path +{ +    /** +     * The number of buffer entries that triggers a cleanup operation. +     */ +    private const CLEANUP_THRESHOLD = 1250; + +    /** +     * The buffer size after the cleanup operation. +     */ +    private const CLEANUP_SIZE = 1000; + +    /** +     * Buffers input/output of {@link canonicalize()}. +     * +     * @var array<string, string> +     */ +    private static array $buffer = []; + +    private static int $bufferSize = 0; + +    /** +     * Canonicalizes the given path. +     * +     * During normalization, all slashes are replaced by forward slashes ("/"). +     * Furthermore, all "." and ".." segments are removed as far as possible. +     * ".." segments at the beginning of relative paths are not removed. +     * +     * ```php +     * echo Path::canonicalize("\symfony\puli\..\css\style.css"); +     * // => /symfony/css/style.css +     * +     * echo Path::canonicalize("../css/./style.css"); +     * // => ../css/style.css +     * ``` +     * +     * This method is able to deal with both UNIX and Windows paths. +     */ +    public static function canonicalize(string $path): string +    { +        if ('' === $path) { +            return ''; +        } + +        // This method is called by many other methods in this class. Buffer +        // the canonicalized paths to make up for the severe performance +        // decrease. +        if (isset(self::$buffer[$path])) { +            return self::$buffer[$path]; +        } + +        // Replace "~" with user's home directory. +        if ('~' === $path[0]) { +            $path = self::getHomeDirectory().substr($path, 1); +        } + +        $path = self::normalize($path); + +        [$root, $pathWithoutRoot] = self::split($path); + +        $canonicalParts = self::findCanonicalParts($root, $pathWithoutRoot); + +        // Add the root directory again +        self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts); +        ++self::$bufferSize; + +        // Clean up regularly to prevent memory leaks +        if (self::$bufferSize > self::CLEANUP_THRESHOLD) { +            self::$buffer = \array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true); +            self::$bufferSize = self::CLEANUP_SIZE; +        } + +        return $canonicalPath; +    } + +    /** +     * Normalizes the given path. +     * +     * During normalization, all slashes are replaced by forward slashes ("/"). +     * Contrary to {@link canonicalize()}, this method does not remove invalid +     * or dot path segments. Consequently, it is much more efficient and should +     * be used whenever the given path is known to be a valid, absolute system +     * path. +     * +     * This method is able to deal with both UNIX and Windows paths. +     */ +    public static function normalize(string $path): string +    { +        return str_replace('\\', '/', $path); +    } + +    /** +     * Returns the directory part of the path. +     * +     * This method is similar to PHP's dirname(), but handles various cases +     * where dirname() returns a weird result: +     * +     *  - dirname() does not accept backslashes on UNIX +     *  - dirname("C:/symfony") returns "C:", not "C:/" +     *  - dirname("C:/") returns ".", not "C:/" +     *  - dirname("C:") returns ".", not "C:/" +     *  - dirname("symfony") returns ".", not "" +     *  - dirname() does not canonicalize the result +     * +     * This method fixes these shortcomings and behaves like dirname() +     * otherwise. +     * +     * The result is a canonical path. +     * +     * @return string The canonical directory part. Returns the root directory +     *                if the root directory is passed. Returns an empty string +     *                if a relative path is passed that contains no slashes. +     *                Returns an empty string if an empty string is passed. +     */ +    public static function getDirectory(string $path): string +    { +        if ('' === $path) { +            return ''; +        } + +        $path = self::canonicalize($path); + +        // Maintain scheme +        if (false !== $schemeSeparatorPosition = strpos($path, '://')) { +            $scheme = substr($path, 0, $schemeSeparatorPosition + 3); +            $path = substr($path, $schemeSeparatorPosition + 3); +        } else { +            $scheme = ''; +        } + +        if (false === $dirSeparatorPosition = strrpos($path, '/')) { +            return ''; +        } + +        // Directory equals root directory "/" +        if (0 === $dirSeparatorPosition) { +            return $scheme.'/'; +        } + +        // Directory equals Windows root "C:/" +        if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) { +            return $scheme.substr($path, 0, 3); +        } + +        return $scheme.substr($path, 0, $dirSeparatorPosition); +    } + +    /** +     * Returns canonical path of the user's home directory. +     * +     * Supported operating systems: +     * +     *  - UNIX +     *  - Windows8 and upper +     * +     * If your operating system or environment isn't supported, an exception is thrown. +     * +     * The result is a canonical path. +     * +     * @throws RuntimeException If your operating system or environment isn't supported +     */ +    public static function getHomeDirectory(): string +    { +        // For UNIX support +        if (getenv('HOME')) { +            return self::canonicalize(getenv('HOME')); +        } + +        // For >= Windows8 support +        if (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."); +    } + +    /** +     * Returns the root directory of a path. +     * +     * The result is a canonical path. +     * +     * @return string The canonical root directory. Returns an empty string if +     *                the given path is relative or empty. +     */ +    public static function getRoot(string $path): string +    { +        if ('' === $path) { +            return ''; +        } + +        // Maintain scheme +        if (false !== $schemeSeparatorPosition = strpos($path, '://')) { +            $scheme = substr($path, 0, $schemeSeparatorPosition + 3); +            $path = substr($path, $schemeSeparatorPosition + 3); +        } else { +            $scheme = ''; +        } + +        $firstCharacter = $path[0]; + +        // UNIX root "/" or "\" (Windows style) +        if ('/' === $firstCharacter || '\\' === $firstCharacter) { +            return $scheme.'/'; +        } + +        $length = \strlen($path); + +        // Windows root +        if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) { +            // Special case: "C:" +            if (2 === $length) { +                return $scheme.$path.'/'; +            } + +            // Normal case: "C:/ or "C:\" +            if ('/' === $path[2] || '\\' === $path[2]) { +                return $scheme.$firstCharacter.$path[1].'/'; +            } +        } + +        return ''; +    } + +    /** +     * Returns the file name without the extension from a file path. +     * +     * @param string|null $extension if specified, only that extension is cut +     *                               off (may contain leading dot) +     */ +    public static function getFilenameWithoutExtension(string $path, ?string $extension = null): string +    { +        if ('' === $path) { +            return ''; +        } + +        if (null !== $extension) { +            // remove extension and trailing dot +            return rtrim(basename($path, $extension), '.'); +        } + +        return pathinfo($path, \PATHINFO_FILENAME); +    } + +    /** +     * Returns the extension from a file path (without leading dot). +     * +     * @param bool $forceLowerCase forces the extension to be lower-case +     */ +    public static function getExtension(string $path, bool $forceLowerCase = false): string +    { +        if ('' === $path) { +            return ''; +        } + +        $extension = pathinfo($path, \PATHINFO_EXTENSION); + +        if ($forceLowerCase) { +            $extension = self::toLower($extension); +        } + +        return $extension; +    } + +    /** +     * Returns whether the path has an (or the specified) extension. +     * +     * @param string               $path       the path string +     * @param string|string[]|null $extensions if null or not provided, checks if +     *                                         an extension exists, otherwise +     *                                         checks for the specified extension +     *                                         or array of extensions (with or +     *                                         without leading dot) +     * @param bool                 $ignoreCase whether to ignore case-sensitivity +     */ +    public static function hasExtension(string $path, $extensions = null, bool $ignoreCase = false): bool +    { +        if ('' === $path) { +            return false; +        } + +        $actualExtension = self::getExtension($path, $ignoreCase); + +        // Only check if path has any extension +        if ([] === $extensions || null === $extensions) { +            return '' !== $actualExtension; +        } + +        if (\is_string($extensions)) { +            $extensions = [$extensions]; +        } + +        foreach ($extensions as $key => $extension) { +            if ($ignoreCase) { +                $extension = self::toLower($extension); +            } + +            // remove leading '.' in extensions array +            $extensions[$key] = ltrim($extension, '.'); +        } + +        return \in_array($actualExtension, $extensions, true); +    } + +    /** +     * Changes the extension of a path string. +     * +     * @param string $path      The path string with filename.ext to change. +     * @param string $extension new extension (with or without leading dot) +     * +     * @return string the path string with new file extension +     */ +    public static function changeExtension(string $path, string $extension): string +    { +        if ('' === $path) { +            return ''; +        } + +        $actualExtension = self::getExtension($path); +        $extension = ltrim($extension, '.'); + +        // No extension for paths +        if ('/' === substr($path, -1)) { +            return $path; +        } + +        // No actual extension in path +        if (empty($actualExtension)) { +            return $path.('.' === substr($path, -1) ? '' : '.').$extension; +        } + +        return substr($path, 0, -\strlen($actualExtension)).$extension; +    } + +    public static function isAbsolute(string $path): bool +    { +        if ('' === $path) { +            return false; +        } + +        // Strip scheme +        if (false !== ($schemeSeparatorPosition = strpos($path, '://')) && 1 !== $schemeSeparatorPosition) { +            $path = substr($path, $schemeSeparatorPosition + 3); +        } + +        $firstCharacter = $path[0]; + +        // UNIX root "/" or "\" (Windows style) +        if ('/' === $firstCharacter || '\\' === $firstCharacter) { +            return true; +        } + +        // Windows root +        if (\strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) { +            // Special case: "C:" +            if (2 === \strlen($path)) { +                return true; +            } + +            // Normal case: "C:/ or "C:\" +            if ('/' === $path[2] || '\\' === $path[2]) { +                return true; +            } +        } + +        return false; +    } + +    public static function isRelative(string $path): bool +    { +        return !self::isAbsolute($path); +    } + +    /** +     * Turns a relative path into an absolute path in canonical form. +     * +     * Usually, the relative path is appended to the given base path. Dot +     * segments ("." and "..") are removed/collapsed and all slashes turned +     * into forward slashes. +     * +     * ```php +     * echo Path::makeAbsolute("../style.css", "/symfony/puli/css"); +     * // => /symfony/puli/style.css +     * ``` +     * +     * If an absolute path is passed, that path is returned unless its root +     * directory is different than the one of the base path. In that case, an +     * exception is thrown. +     * +     * ```php +     * Path::makeAbsolute("/style.css", "/symfony/puli/css"); +     * // => /style.css +     * +     * Path::makeAbsolute("C:/style.css", "C:/symfony/puli/css"); +     * // => C:/style.css +     * +     * Path::makeAbsolute("C:/style.css", "/symfony/puli/css"); +     * // InvalidArgumentException +     * ``` +     * +     * If the base path is not an absolute path, an exception is thrown. +     * +     * The result is a canonical path. +     * +     * @param string $basePath an absolute base path +     * +     * @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)); +        } + +        if (!self::isAbsolute($basePath)) { +            throw new InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath)); +        } + +        if (self::isAbsolute($path)) { +            return self::canonicalize($path); +        } + +        if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) { +            $scheme = substr($basePath, 0, $schemeSeparatorPosition + 3); +            $basePath = substr($basePath, $schemeSeparatorPosition + 3); +        } else { +            $scheme = ''; +        } + +        return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path); +    } + +    /** +     * Turns a path into a relative path. +     * +     * The relative path is created relative to the given base path: +     * +     * ```php +     * echo Path::makeRelative("/symfony/style.css", "/symfony/puli"); +     * // => ../style.css +     * ``` +     * +     * If a relative path is passed and the base path is absolute, the relative +     * path is returned unchanged: +     * +     * ```php +     * Path::makeRelative("style.css", "/symfony/puli/css"); +     * // => style.css +     * ``` +     * +     * If both paths are relative, the relative path is created with the +     * assumption that both paths are relative to the same directory: +     * +     * ```php +     * Path::makeRelative("style.css", "symfony/puli/css"); +     * // => ../../../style.css +     * ``` +     * +     * If both paths are absolute, their root directory must be the same, +     * otherwise an exception is thrown: +     * +     * ```php +     * Path::makeRelative("C:/symfony/style.css", "/symfony/puli"); +     * // InvalidArgumentException +     * ``` +     * +     * If the passed path is absolute, but the base path is not, an exception +     * is thrown as well: +     * +     * ```php +     * Path::makeRelative("/symfony/style.css", "symfony/puli"); +     * // InvalidArgumentException +     * ``` +     * +     * If the base path is not an absolute path, an exception is thrown. +     * +     * The result is a canonical path. +     * +     * @throws InvalidArgumentException if the base path is not absolute or if +     *                                  the given path has a different root +     *                                  than the base path +     */ +    public static function makeRelative(string $path, string $basePath): string +    { +        $path = self::canonicalize($path); +        $basePath = self::canonicalize($basePath); + +        [$root, $relativePath] = self::split($path); +        [$baseRoot, $relativeBasePath] = self::split($basePath); + +        // If the base path is given as absolute path and the path is already +        // relative, consider it to be relative to the given absolute path +        // already +        if ('' === $root && '' !== $baseRoot) { +            // If base path is already in its root +            if ('' === $relativeBasePath) { +                $relativePath = ltrim($relativePath, './\\'); +            } + +            return $relativePath; +        } + +        // 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)); +        } + +        // 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)); +        } + +        if ('' === $relativeBasePath) { +            return $relativePath; +        } + +        // Build a "../../" prefix with as many "../" parts as necessary +        $parts = explode('/', $relativePath); +        $baseParts = explode('/', $relativeBasePath); +        $dotDotPrefix = ''; + +        // Once we found a non-matching part in the prefix, we need to add +        // "../" parts for all remaining parts +        $match = true; + +        foreach ($baseParts as $index => $basePart) { +            if ($match && isset($parts[$index]) && $basePart === $parts[$index]) { +                unset($parts[$index]); + +                continue; +            } + +            $match = false; +            $dotDotPrefix .= '../'; +        } + +        return rtrim($dotDotPrefix.implode('/', $parts), '/'); +    } + +    /** +     * Returns whether the given path is on the local filesystem. +     */ +    public static function isLocal(string $path): bool +    { +        return '' !== $path && !str_contains($path, '://'); +    } + +    /** +     * Returns the longest common base path in canonical form of a set of paths or +     * `null` if the paths are on different Windows partitions. +     * +     * Dot segments ("." and "..") are removed/collapsed and all slashes turned +     * into forward slashes. +     * +     * ```php +     * $basePath = Path::getLongestCommonBasePath( +     *     '/symfony/css/style.css', +     *     '/symfony/css/..' +     * ); +     * // => /symfony +     * ``` +     * +     * The root is returned if no common base path can be found: +     * +     * ```php +     * $basePath = Path::getLongestCommonBasePath( +     *     '/symfony/css/style.css', +     *     '/puli/css/..' +     * ); +     * // => / +     * ``` +     * +     * If the paths are located on different Windows partitions, `null` is +     * returned. +     * +     * ```php +     * $basePath = Path::getLongestCommonBasePath( +     *     'C:/symfony/css/style.css', +     *     'D:/symfony/css/..' +     * ); +     * // => null +     * ``` +     */ +    public static function getLongestCommonBasePath(string ...$paths): ?string +    { +        [$bpRoot, $basePath] = self::split(self::canonicalize(reset($paths))); + +        for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) { +            [$root, $path] = self::split(self::canonicalize(current($paths))); + +            // If we deal with different roots (e.g. C:/ vs. D:/), it's time +            // to quit +            if ($root !== $bpRoot) { +                return null; +            } + +            // Make the base path shorter until it fits into path +            while (true) { +                if ('.' === $basePath) { +                    // No more base paths +                    $basePath = ''; + +                    // next path +                    continue 2; +                } + +                // Prevent false positives for common prefixes +                // see isBasePath() +                if (str_starts_with($path.'/', $basePath.'/')) { +                    // next path +                    continue 2; +                } + +                $basePath = \dirname($basePath); +            } +        } + +        return $bpRoot.$basePath; +    } + +    /** +     * Joins two or more path strings into a canonical path. +     */ +    public static function join(string ...$paths): string +    { +        $finalPath = null; +        $wasScheme = false; + +        foreach ($paths as $path) { +            if ('' === $path) { +                continue; +            } + +            if (null === $finalPath) { +                // For first part we keep slashes, like '/top', 'C:\' or 'phar://' +                $finalPath = $path; +                $wasScheme = str_contains($path, '://'); +                continue; +            } + +            // Only add slash if previous part didn't end with '/' or '\' +            if (!\in_array(substr($finalPath, -1), ['/', '\\'])) { +                $finalPath .= '/'; +            } + +            // If first part included a scheme like 'phar://' we allow \current part to start with '/', otherwise trim +            $finalPath .= $wasScheme ? $path : ltrim($path, '/'); +            $wasScheme = false; +        } + +        if (null === $finalPath) { +            return ''; +        } + +        return self::canonicalize($finalPath); +    } + +    /** +     * Returns whether a path is a base path of another path. +     * +     * Dot segments ("." and "..") are removed/collapsed and all slashes turned +     * into forward slashes. +     * +     * ```php +     * Path::isBasePath('/symfony', '/symfony/css'); +     * // => true +     * +     * Path::isBasePath('/symfony', '/symfony'); +     * // => true +     * +     * Path::isBasePath('/symfony', '/symfony/..'); +     * // => false +     * +     * Path::isBasePath('/symfony', '/puli'); +     * // => false +     * ``` +     */ +    public static function isBasePath(string $basePath, string $ofPath): bool +    { +        $basePath = self::canonicalize($basePath); +        $ofPath = self::canonicalize($ofPath); + +        // Append slashes to prevent false positives when two paths have +        // a common prefix, for example /base/foo and /base/foobar. +        // 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 string[] +     */ +    private static function findCanonicalParts(string $root, string $pathWithoutRoot): array +    { +        $parts = explode('/', $pathWithoutRoot); + +        $canonicalParts = []; + +        // Collapse "." and "..", if possible +        foreach ($parts as $part) { +            if ('.' === $part || '' === $part) { +                continue; +            } + +            // Collapse ".." with the previous part, if one exists +            // Don't collapse ".." if the previous part is also ".." +            if ('..' === $part && \count($canonicalParts) > 0 && '..' !== $canonicalParts[\count($canonicalParts) - 1]) { +                array_pop($canonicalParts); + +                continue; +            } + +            // Only add ".." prefixes for relative paths +            if ('..' !== $part || '' === $root) { +                $canonicalParts[] = $part; +            } +        } + +        return $canonicalParts; +    } + +    /** +     * Splits a canonical path into its root directory and the remainder. +     * +     * If the path has no root directory, an empty root directory will be +     * returned. +     * +     * If the root directory is a Windows style partition, the resulting root +     * will always contain a trailing slash. +     * +     * list ($root, $path) = Path::split("C:/symfony") +     * // => ["C:/", "symfony"] +     * +     * list ($root, $path) = Path::split("C:") +     * // => ["C:/", ""] +     * +     * @return array{string, string} an array with the root directory and the remaining relative path +     */ +    private static function split(string $path): array +    { +        if ('' === $path) { +            return ['', '']; +        } + +        // Remember scheme as part of the root, if any +        if (false !== $schemeSeparatorPosition = strpos($path, '://')) { +            $root = substr($path, 0, $schemeSeparatorPosition + 3); +            $path = substr($path, $schemeSeparatorPosition + 3); +        } else { +            $root = ''; +        } + +        $length = \strlen($path); + +        // Remove and remember root directory +        if (str_starts_with($path, '/')) { +            $root .= '/'; +            $path = $length > 1 ? substr($path, 1) : ''; +        } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { +            if (2 === $length) { +                // Windows special case: "C:" +                $root .= $path.'/'; +                $path = ''; +            } elseif ('/' === $path[2]) { +                // Windows normal case: "C:/".. +                $root .= substr($path, 0, 3); +                $path = $length > 3 ? substr($path, 3) : ''; +            } +        } + +        return [$root, $path]; +    } + +    private static function toLower(string $string): string +    { +        if (false !== $encoding = mb_detect_encoding($string, null, true)) { +            return mb_strtolower($string, $encoding); +        } + +        return strtolower($string); +    } + +    private function __construct() +    { +    } +} diff --git a/vendor/symfony/filesystem/README.md b/vendor/symfony/filesystem/README.md new file mode 100644 index 000000000..f2f6d45f7 --- /dev/null +++ b/vendor/symfony/filesystem/README.md @@ -0,0 +1,13 @@ +Filesystem Component +==================== + +The Filesystem component provides basic utilities for the filesystem. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/filesystem.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and +   [send Pull Requests](https://github.com/symfony/symfony/pulls) +   in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/filesystem/composer.json b/vendor/symfony/filesystem/composer.json new file mode 100644 index 000000000..fd75755b2 --- /dev/null +++ b/vendor/symfony/filesystem/composer.json @@ -0,0 +1,33 @@ +{ +    "name": "symfony/filesystem", +    "type": "library", +    "description": "Provides basic utilities for the filesystem", +    "keywords": [], +    "homepage": "https://symfony.com", +    "license": "MIT", +    "authors": [ +        { +            "name": "Fabien Potencier", +            "email": "fabien@symfony.com" +        }, +        { +            "name": "Symfony Community", +            "homepage": "https://symfony.com/contributors" +        } +    ], +    "require": { +        "php": ">=8.1", +        "symfony/polyfill-ctype": "~1.8", +        "symfony/polyfill-mbstring": "~1.8" +    }, +    "require-dev": { +        "symfony/process": "^5.4|^6.4|^7.0" +    }, +    "autoload": { +        "psr-4": { "Symfony\\Component\\Filesystem\\": "" }, +        "exclude-from-classmap": [ +            "/Tests/" +        ] +    }, +    "minimum-stability": "dev" +}  | 
