<commit_before><?php
namespace Neos\Utility;

/*
 * This file is part of the Neos.Utility.Files package.
 *
 * (c) Contributors of the Neos Project - www.neos.io
 *
 * This package is Open Source Software. For the full copyright and license
 * information, please view the LICENSE file which was distributed with this
 * source code.
 */

use Neos\Flow\Error\Exception as ErrorException;
use Neos\Utility\Exception\FilesException;

/**
 * File and directory functions
 */
abstract class Files
{
    /**
     * Replacing backslashes and double slashes to slashes.
     * It's needed to compare paths (especially on windows).
     *
     * @param string $path Path which should transformed to the Unix Style.
     * @return string
     * @api
     */
    public static function getUnixStylePath(string $path): string
    {
        if (strpos($path, ':') === false) {
            return str_replace(['//', '\\'], '/', $path);
        }
        return preg_replace('/^([a-z]{2,}):\//', '$1://', str_replace(['//', '\\'], '/', $path));
    }

    /**
     * Makes sure path has a trailing slash
     *
     * @param string $path
     * @return string
     * @api
     */
    public static function getNormalizedPath(string $path): string
    {
        return rtrim($path, '/') . '/';
    }

    /**
     * Properly glues together filepaths / filenames by replacing
     * backslashes and double slashes of the specified paths.
     * Note: trailing slashes will be removed, leading slashes won't.
     * Usage: concatenatePaths(array('dir1/dir2', 'dir3', 'file'))
     *
     * @param array $paths the file paths to be combined. Last array element may include the filename.
     * @return string concatenated path without trailing slash.
     * @see getUnixStylePath()
     * @api
     */
    public static function concatenatePaths(array $paths): string
    {
        $resultingPath = '';
        foreach ($paths as $index => $path) {
            $path = self::getUnixStylePath($path);
            if ($index === 0) {
                $path = rtrim($path, '/');
            } else {
                $path = trim($path, '/');
            }
            if ($path !== '') {
                $resultingPath .= $path . '/';
            }
        }
        return rtrim($resultingPath, '/');
    }

    /**
     * Returns all filenames from the specified directory. Filters hidden files and
     * directories.
     *
     * @param string $path Path to the directory which shall be read
     * @param string $suffix If specified, only filenames with this extension are returned (eg. ".php" or "foo.bar")
     * @param boolean $returnRealPath If turned on, all paths are resolved by calling realpath()
     * @param boolean $returnDotFiles If turned on, also files beginning with a dot will be returned
     * @return array Filenames including full path
     * @api
     */
    public static function readDirectoryRecursively(string $path, string $suffix = null, bool $returnRealPath = false, bool $returnDotFiles = false): array
    {
        return iterator_to_array(self::getRecursiveDirectoryGenerator($path, $suffix, $returnRealPath, $returnDotFiles));
    }

    /**
     * @param string $path
     * @param string $suffix
     * @param boolean $returnRealPath
     * @param boolean $returnDotFiles
     * @return \Generator
     * @throws FilesException
     */
    public static function getRecursiveDirectoryGenerator(string $path, string $suffix = null, bool $returnRealPath = false, bool $returnDotFiles = false)
    {
        if (!is_dir($path)) {
            throw new FilesException('"' . $path . '" is no directory.', 1207253462);
        }

        $directories = [self::getNormalizedPath($path)];
        while ($directories !== []) {
            $currentDirectory = array_pop($directories);
            if ($handle = opendir($currentDirectory)) {
                while (false !== ($filename = readdir($handle))) {
                    if ($filename === '.' || $filename === '..') {
                        continue;
                    }

                    if ($filename[0] === '.' && $returnDotFiles === false) {
                        continue;
                    }

                    $pathAndFilename = self::concatenatePaths([$currentDirectory, $filename]);
                    if (is_dir($pathAndFilename)) {
                        array_push($directories, self::getNormalizedPath($pathAndFilename));
                    } elseif ($suffix === null || strpos(strrev($filename), strrev($suffix)) === 0) {
                        yield static::getUnixStylePath(($returnRealPath === true) ? realpath($pathAndFilename) : $pathAndFilename);
                    }
                }
                closedir($handle);
            }
        }
    }

    /**
     * Deletes all files, directories and subdirectories from the specified
     * directory. The passed directory itself won't be deleted though.
     *
     * @param string $path Path to the directory which shall be emptied.
     * @return void
     * @throws FilesException
     * @see removeDirectoryRecursively()
     * @api
     */
    public static function emptyDirectoryRecursively(string $path)
    {
        if (!is_dir($path)) {
            throw new FilesException('"' . $path . '" is no directory.', 1169047616);
        }

        if (self::is_link($path)) {
            if (self::unlink($path) !== true) {
                throw new FilesException('Could not unlink symbolic link "' . $path . '".', 1323697654);
            }
        } else {
            $directoryIterator = new \RecursiveDirectoryIterator($path);
            foreach ($directoryIterator as $fileInfo) {
                if (!$fileInfo->isDir()) {
                    if (self::unlink($fileInfo->getPathname()) !== true) {
                        throw new FilesException('Could not unlink file "' . $fileInfo->getPathname() . '".', 1169047619);
                    }
                } elseif (!$directoryIterator->isDot()) {
                    self::removeDirectoryRecursively($fileInfo->getPathname());
                }
            }
        }
    }

    /**
     * Removes all empty directories on the specified path. If a base path is given, this function will not remove
     * directories, even if empty, above and including that base path.
     *
     * Any .DS_Store files are silently removed.
     *
     * @param string $path The path on which empty directories shall be removed
     * @param string $basePath A parent path of $path where removal of directories stops
     * @return void
     * @see removeDirectoryRecursively()
     * @api
     * @throws FilesException
     */
    public static function removeEmptyDirectoriesOnPath(string $path, string $basePath = null)
    {
        if ($basePath !== null) {
            $basePath = rtrim($basePath, '/');
            if (strpos($path, $basePath) !== 0) {
                throw new FilesException(sprintf('Could not remove empty directories on path because the given base path "%s" is not a parent path of "%s".', $basePath, $path), 1323962907);
            }
        }
        foreach (array_reverse(explode('/', $path)) as $currentSegment) {
            if ($path === $basePath) {
                break;
            }
            if (file_exists($path . '/.DS_Store')) {
                try {
                    @unlink($path . '/.DS_Store');
                } catch (\Throwable $e) {
                    // PHP 8 apparently throws for unlink even with shutup operator, but we really don't care at this place. It's also the only way to handle this race-condition free.
                }
            }
            if (@rmdir($path) === false) {
                break;
            }
            $path = substr($path, 0, -(strlen($currentSegment) + 1));
        }
    }

    /**
     * Deletes all files, directories and subdirectories from the specified
     * directory. Contrary to emptyDirectoryRecursively() this function will
     * also finally remove the emptied directory.
     *
     * @param  string $path Path to the directory which shall be removed completely.
     * @return void
     * @throws FilesException<commit_msg>BUGFIX: Catch Warning for rmdir in PHP 8.0<commit_after><?php
namespace Neos\Utility;

/*
 * This file is part of the Neos.Utility.Files package.
 *
 * (c) Contributors of the Neos Project - www.neos.io
 *
 * This package is Open Source Software. For the full copyright and license
 * information, please view the LICENSE file which was distributed with this
 * source code.
 */

use Neos\Flow\Error\Exception as ErrorException;
use Neos\Utility\Exception\FilesException;

/**
 * File and directory functions
 */
abstract class Files
{
    /**
     * Replacing backslashes and double slashes to slashes.
     * It's needed to compare paths (especially on windows).
     *
     * @param string $path Path which should transformed to the Unix Style.
     * @return string
     * @api
     */
    public static function getUnixStylePath(string $path): string
    {
        if (strpos($path, ':') === false) {
            return str_replace(['//', '\\'], '/', $path);
        }
        return preg_replace('/^([a-z]{2,}):\//', '$1://', str_replace(['//', '\\'], '/', $path));
    }

    /**
     * Makes sure path has a trailing slash
     *
     * @param string $path
     * @return string
     * @api
     */
    public static function getNormalizedPath(string $path): string
    {
        return rtrim($path, '/') . '/';
    }

    /**
     * Properly glues together filepaths / filenames by replacing
     * backslashes and double slashes of the specified paths.
     * Note: trailing slashes will be removed, leading slashes won't.
     * Usage: concatenatePaths(array('dir1/dir2', 'dir3', 'file'))
     *
     * @param array $paths the file paths to be combined. Last array element may include the filename.
     * @return string concatenated path without trailing slash.
     * @see getUnixStylePath()
     * @api
     */
    public static function concatenatePaths(array $paths): string
    {
        $resultingPath = '';
        foreach ($paths as $index => $path) {
            $path = self::getUnixStylePath($path);
            if ($index === 0) {
                $path = rtrim($path, '/');
            } else {
                $path = trim($path, '/');
            }
            if ($path !== '') {
                $resultingPath .= $path . '/';
            }
        }
        return rtrim($resultingPath, '/');
    }

    /**
     * Returns all filenames from the specified directory. Filters hidden files and
     * directories.
     *
     * @param string $path Path to the directory which shall be read
     * @param string $suffix If specified, only filenames with this extension are returned (eg. ".php" or "foo.bar")
     * @param boolean $returnRealPath If turned on, all paths are resolved by calling realpath()
     * @param boolean $returnDotFiles If turned on, also files beginning with a dot will be returned
     * @return array Filenames including full path
     * @api
     */
    public static function readDirectoryRecursively(string $path, string $suffix = null, bool $returnRealPath = false, bool $returnDotFiles = false): array
    {
        return iterator_to_array(self::getRecursiveDirectoryGenerator($path, $suffix, $returnRealPath, $returnDotFiles));
    }

    /**
     * @param string $path
     * @param string $suffix
     * @param boolean $returnRealPath
     * @param boolean $returnDotFiles
     * @return \Generator
     * @throws FilesException
     */
    public static function getRecursiveDirectoryGenerator(string $path, string $suffix = null, bool $returnRealPath = false, bool $returnDotFiles = false)
    {
        if (!is_dir($path)) {
            throw new FilesException('"' . $path . '" is no directory.', 1207253462);
        }

        $directories = [self::getNormalizedPath($path)];
        while ($directories !== []) {
            $currentDirectory = array_pop($directories);
            if ($handle = opendir($currentDirectory)) {
                while (false !== ($filename = readdir($handle))) {
                    if ($filename === '.' || $filename === '..') {
                        continue;
                    }

                    if ($filename[0] === '.' && $returnDotFiles === false) {
                        continue;
                    }

                    $pathAndFilename = self::concatenatePaths([$currentDirectory, $filename]);
                    if (is_dir($pathAndFilename)) {
                        array_push($directories, self::getNormalizedPath($pathAndFilename));
                    } elseif ($suffix === null || strpos(strrev($filename), strrev($suffix)) === 0) {
                        yield static::getUnixStylePath(($returnRealPath === true) ? realpath($pathAndFilename) : $pathAndFilename);
                    }
                }
                closedir($handle);
            }
        }
    }

    /**
     * Deletes all files, directories and subdirectories from the specified
     * directory. The passed directory itself won't be deleted though.
     *
     * @param string $path Path to the directory which shall be emptied.
     * @return void
     * @throws FilesException
     * @see removeDirectoryRecursively()
     * @api
     */
    public static function emptyDirectoryRecursively(string $path)
    {
        if (!is_dir($path)) {
            throw new FilesException('"' . $path . '" is no directory.', 1169047616);
        }

        if (self::is_link($path)) {
            if (self::unlink($path) !== true) {
                throw new FilesException('Could not unlink symbolic link "' . $path . '".', 1323697654);
            }
        } else {
            $directoryIterator = new \RecursiveDirectoryIterator($path);
            foreach ($directoryIterator as $fileInfo) {
                if (!$fileInfo->isDir()) {
                    if (self::unlink($fileInfo->getPathname()) !== true) {
                        throw new FilesException('Could not unlink file "' . $fileInfo->getPathname() . '".', 1169047619);
                    }
                } elseif (!$directoryIterator->isDot()) {
                    self::removeDirectoryRecursively($fileInfo->getPathname());
                }
            }
        }
    }

    /**
     * Removes all empty directories on the specified path. If a base path is given, this function will not remove
     * directories, even if empty, above and including that base path.
     *
     * Any .DS_Store files are silently removed.
     *
     * @param string $path The path on which empty directories shall be removed
     * @param string $basePath A parent path of $path where removal of directories stops
     * @return void
     * @see removeDirectoryRecursively()
     * @api
     * @throws FilesException
     */
    public static function removeEmptyDirectoriesOnPath(string $path, string $basePath = null)
    {
        if ($basePath !== null) {
            $basePath = rtrim($basePath, '/');
            if (strpos($path, $basePath) !== 0) {
                throw new FilesException(sprintf('Could not remove empty directories on path because the given base path "%s" is not a parent path of "%s".', $basePath, $path), 1323962907);
            }
        }
        foreach (array_reverse(explode('/', $path)) as $currentSegment) {
            if ($path === $basePath) {
                break;
            }
            if (file_exists($path . '/.DS_Store')) {
                try {
                    @unlink($path . '/.DS_Store');
                } catch (\Throwable $e) {
                    // PHP 8 apparently throws for unlink even with shutup operator, but we really don't care at this place. It's also the only way to handle this race-condition free.
                }
            }
            try {
                rmdir($path);
            } catch (\Throwable $e) {
                break;
            }
            $path = substr($path, 0, -(strlen($currentSegment) + 1));
        }
    }

    /**
     * Deletes all files, directories and subdirectories from the specified
     * directory. Contrary to emptyDirectoryRecursively() this function will
     * also finally remove the emptied directory.
     *
     * @param  string $path Path to the directory which shall be removed completely.
     * @return void
     * @throws FilesException
{'fraction_non_alphanumeric': 0.11855607134131443, 'fraction_numerical': 0.007696066454923039, 'mean_word_length': 2.0495436766623207, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4608488', 'n_tokens_mistral': 4464, 'n_tokens_neox': 4230, 'n_words': 1350}