Added Symfony/Filesystem

This commit is contained in:
Netkas 2022-08-12 10:48:57 -04:00
parent 86a2132778
commit 7f2e73c045
15 changed files with 1833 additions and 139 deletions

View file

@ -20,6 +20,7 @@
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'polyfill-mbstring' . DIRECTORY_SEPARATOR . 'bootstrap.php',
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'Process' . DIRECTORY_SEPARATOR . 'autoload_spl.php',
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'Uid' . DIRECTORY_SEPARATOR . 'autoload_spl.php',
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'Filesystem' . DIRECTORY_SEPARATOR . 'autoload_spl.php',
];
foreach($target_files as $file)

View file

@ -1,139 +0,0 @@
#!/bin/php
# ------------------------------------------------------------------
# Nosial Code Compiler (NCC) Installation Script
#
# Nosial Code Compiler is a program written in PHP designed
# to be a multi-purpose compiler, package manager and toolkit.
#
# Dependency:
# PHP 8.0+
# ------------------------------------------------------------------
<?PHP
// TODO: Check for root before proceeding
# Global Variables
$NCC_INSTALL_PATH='/etc/ncc';
$NCC_DATA_PATH='/var/ncc';
$NCC_UPDATE_SOURCE='https://updates.nosial.com/ncc/check_updates?current_version=$VERSION'; # Unused
$NCC_CHECKSUM=__DIR__ . DIRECTORY_SEPARATOR . 'checksum.bin';
$NCC_AUTOLOAD=__DIR__ . DIRECTORY_SEPARATOR . 'autoload.php';
// Require NCC
if(!file_exists($NCC_AUTOLOAD))
{
print('The file \'autoload.php\' was not found, installation cannot proceed.' . PHP_EOL);
exit(1);
}
require($NCC_AUTOLOAD);
// Check for the required files
$required_files = [
'LICENSE',
'build_files'
];
foreach($required_files as $file)
{
if(!file_exists($file))
{
\ncc\Utilities\Console::outError('Missing file \'' . $file . '\', installation failed.', true, 1);
}
}
// Preform the checksum validation
if(!file_exists($NCC_CHECKSUM))
{
\ncc\Utilities\Console::outWarning('The file \'checksum.bin\' was not found, the contents of the program cannot be verified to be safe');
}
else
{
\ncc\Utilities\Console::out('Running checksum');
$checksum = \ncc\ZiProto\ZiProto::decode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'checksum.bin'));
$checksum_failed = false;
foreach($checksum as $file => $hash)
{
if(file_exists(__DIR__ . DIRECTORY_SEPARATOR . $file) == false)
{
\ncc\Utilities\Console::outError('Cannot check file, \'' . $file . '\' not found.');
$checksum_failed = true;
}
elseif(hash_file('sha256', __DIR__ . DIRECTORY_SEPARATOR . $file) !== $hash)
{
\ncc\Utilities\Console::outWarning('The file \'' . $file . '\' does not match the original checksum');
$checksum_failed = true;
}
}
if($checksum_failed)
{
\ncc\Utilities\Console::outError('Checksum failed, the contents of the program cannot be verified to be safe');
exit(1);
}
else
{
\ncc\Utilities\Console::out('Checksum passed');
}
}
// Check for required extensions
foreach(\ncc\Utilities\Validate::requiredExtensions() as $ext => $installed)
{
if(!$installed)
{
\ncc\Utilities\Console::outWarning('The extension \'' . $ext . '\' is not installed, compatibility without it is not guaranteed');
}
}
// Start of installer
\ncc\Utilities\Console::out('Started NCC installer');
// Determine the installation path
// TODO: Add the ability to change the data path as well
while(true)
{
$user_input = null;
$user_input = \ncc\Utilities\Console::getInput("Installation Path (Default: $NCC_INSTALL_PATH): ");
if(strlen($user_input) > 0)
{
if(file_exists($user_input))
{
if(file_exists($user_input . DIRECTORY_SEPARATOR . 'ncc'))
{
\ncc\Utilities\Console::out('NCC Seems to already be installed, the installer will repair/upgrade your current install');
break;
}
else
{
\ncc\Utilities\Console::outError('The given directory already exists, it must be deleted before proceeding');
}
}
else
{
break;
}
}
else
{
break;
}
}
\ncc\Utilities\Console::out("Note: This doesn't affect your current install of composer (if you have composer installed)");
$update_composer = \ncc\Utilities\Console::getBooleanInput('Do you want to install composer for NCC? (Recommended)');
// Prepare installation
if(file_exists($NCC_INSTALL_PATH))
{
// TODO: Implement recursive directory removal
}
mkdir($NCC_INSTALL_PATH);
mkdir($NCC_DATA_PATH);
// Install composer
// Install NCC
?>

View file

@ -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

View file

@ -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 ncc\ThirdParty\Symfony\Filesystem\Exception;
/**
* Exception interface for all exceptions thrown by the component.
*
* @author Romain Neutron <imprec@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View file

@ -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 ncc\ThirdParty\Symfony\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);
}
}

View file

@ -0,0 +1,39 @@
<?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 ncc\ThirdParty\Symfony\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);
}
/**
* {@inheritdoc}
*/
public function getPath(): ?string
{
return $this->path;
}
}

View file

@ -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 ncc\ThirdParty\Symfony\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;
}

View file

@ -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 ncc\ThirdParty\Symfony\Filesystem\Exception;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -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 ncc\ThirdParty\Symfony\Filesystem\Exception;
/**
* @author Théo Fidry <theo.fidry@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View file

@ -0,0 +1,737 @@
<?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 ncc\ThirdParty\Symfony\Filesystem;
use ncc\ThirdParty\Symfony\Filesystem\Exception\FileNotFoundException;
use ncc\ThirdParty\Symfony\Filesystem\Exception\InvalidArgumentException;
use ncc\ThirdParty\Symfony\Filesystem\Exception\IOException;
/**
* Provides basic utility to manipulate the file system.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Filesystem
{
private static $lastError;
/**
* 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.
*
* @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 && null === 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));
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.
*
* @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
*
* @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.
*
* @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) && (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
*
* @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 (\is_int($mode) && !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.
*
* @param string|int $user A user name or number
* @param bool $recursive Whether change the owner recursively or not
*
* @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.
*
* @param string|int $group A group name or number
* @param bool $recursive Whether change the group recursively or not
*
* @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.
*
* @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.
*
* @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)
*
* @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)
{
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 = function ($path) {
return (\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)
*
* @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
*
* @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_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, file_exists($filename) ? fileperms($filename) : 0666 & ~umask());
$this->rename($tmpFile, $filename, true);
} finally {
if (file_exists($tmpFile)) {
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
*
* @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(__CLASS__.'::handleError');
try {
return $func(...$args);
} finally {
restore_error_handler();
}
}
/**
* @internal
*/
public static function handleError(int $type, string $msg)
{
self::$lastError = $msg;
}
}

View file

@ -0,0 +1,19 @@
Copyright (c) 2004-2022 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.

View file

@ -0,0 +1,819 @@
<?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 ncc\ThirdParty\Symfony\Filesystem;
use ncc\ThirdParty\Symfony\Filesystem\Exception\InvalidArgumentException;
use ncc\ThirdParty\Symfony\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 $buffer = [];
/**
* @var int
*/
private static $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().mb_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 = mb_strpos($path, '://'))) {
$scheme = mb_substr($path, 0, $schemeSeparatorPosition + 3);
$path = mb_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.mb_substr($path, 0, 3);
}
return $scheme.mb_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 = mb_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 ('/' === mb_substr($path, -1)) {
return $path;
}
// No actual extension in path
if (empty($actualExtension)) {
return $path.('.' === mb_substr($path, -1) ? '' : '.').$extension;
}
return mb_substr($path, 0, -mb_strlen($actualExtension)).$extension;
}
public static function isAbsolute(string $path): bool
{
if ('' === $path) {
return false;
}
// Strip scheme
if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) {
$path = mb_substr($path, $schemeSeparatorPosition + 3);
}
$firstCharacter = $path[0];
// UNIX root "/" or "\" (Windows style)
if ('/' === $firstCharacter || '\\' === $firstCharacter) {
return true;
}
// Windows root
if (mb_strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) {
// Special case: "C:"
if (2 === mb_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 = mb_strpos($basePath, '://'))) {
$scheme = mb_substr($basePath, 0, $schemeSeparatorPosition + 3);
$basePath = mb_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(mb_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 non-empty-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 = mb_strpos($path, '://'))) {
$root = mb_substr($path, 0, $schemeSeparatorPosition + 3);
$path = mb_substr($path, $schemeSeparatorPosition + 3);
} else {
$root = '';
}
$length = mb_strlen($path);
// Remove and remember root directory
if (str_starts_with($path, '/')) {
$root .= '/';
$path = $length > 1 ? mb_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 .= mb_substr($path, 0, 3);
$path = $length > 3 ? mb_substr($path, 3) : '';
}
}
return [$root, $path];
}
private static function toLower(string $string): string
{
if (false !== $encoding = mb_detect_encoding($string)) {
return mb_strtolower($string, $encoding);
}
return strtolower($string, $encoding);
}
private function __construct()
{
}
}

View file

@ -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)

View file

@ -0,0 +1 @@
6.1.3

View file

@ -22,6 +22,10 @@
{
"vendor": "Symfony",
"package_name": "polyfill-uid"
},
{
"vendor": "Symfony",
"package_name": "Filesystem"
}
],
"update_source": null