vendor/symfony/filesystem/Filesystem.php line 637

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Filesystem;
  11. use Symfony\Component\Filesystem\Exception\FileNotFoundException;
  12. use Symfony\Component\Filesystem\Exception\InvalidArgumentException;
  13. use Symfony\Component\Filesystem\Exception\IOException;
  14. /**
  15.  * Provides basic utility to manipulate the file system.
  16.  *
  17.  * @author Fabien Potencier <fabien@symfony.com>
  18.  */
  19. class Filesystem
  20. {
  21.     private static $lastError;
  22.     /**
  23.      * Copies a file.
  24.      *
  25.      * If the target file is older than the origin file, it's always overwritten.
  26.      * If the target file is newer, it is overwritten only when the
  27.      * $overwriteNewerFiles option is set to true.
  28.      *
  29.      * @throws FileNotFoundException When originFile doesn't exist
  30.      * @throws IOException           When copy fails
  31.      */
  32.     public function copy(string $originFilestring $targetFilebool $overwriteNewerFiles false)
  33.     {
  34.         $originIsLocal stream_is_local($originFile) || === stripos($originFile'file://');
  35.         if ($originIsLocal && !is_file($originFile)) {
  36.             throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.'$originFile), 0null$originFile);
  37.         }
  38.         $this->mkdir(\dirname($targetFile));
  39.         $doCopy true;
  40.         if (!$overwriteNewerFiles && null === parse_url($originFile\PHP_URL_HOST) && is_file($targetFile)) {
  41.             $doCopy filemtime($originFile) > filemtime($targetFile);
  42.         }
  43.         if ($doCopy) {
  44.             // https://bugs.php.net/64634
  45.             if (!$source self::box('fopen'$originFile'r')) {
  46.                 throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: '$originFile$targetFile).self::$lastError0null$originFile);
  47.             }
  48.             // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
  49.             if (!$target self::box('fopen'$targetFile'w'falsestream_context_create(['ftp' => ['overwrite' => true]]))) {
  50.                 throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: '$originFile$targetFile).self::$lastError0null$originFile);
  51.             }
  52.             $bytesCopied stream_copy_to_stream($source$target);
  53.             fclose($source);
  54.             fclose($target);
  55.             unset($source$target);
  56.             if (!is_file($targetFile)) {
  57.                 throw new IOException(sprintf('Failed to copy "%s" to "%s".'$originFile$targetFile), 0null$originFile);
  58.             }
  59.             if ($originIsLocal) {
  60.                 // Like `cp`, preserve executable permission bits
  61.                 self::box('chmod'$targetFilefileperms($targetFile) | (fileperms($originFile) & 0111));
  62.                 if ($bytesCopied !== $bytesOrigin filesize($originFile)) {
  63.                     throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).'$originFile$targetFile$bytesCopied$bytesOrigin), 0null$originFile);
  64.                 }
  65.             }
  66.         }
  67.     }
  68.     /**
  69.      * Creates a directory recursively.
  70.      *
  71.      * @throws IOException On any directory creation failure
  72.      */
  73.     public function mkdir(string|iterable $dirsint $mode 0777)
  74.     {
  75.         foreach ($this->toIterable($dirs) as $dir) {
  76.             if (is_dir($dir)) {
  77.                 continue;
  78.             }
  79.             if (!self::box('mkdir'$dir$modetrue) && !is_dir($dir)) {
  80.                 throw new IOException(sprintf('Failed to create "%s": '$dir).self::$lastError0null$dir);
  81.             }
  82.         }
  83.     }
  84.     /**
  85.      * Checks the existence of files or directories.
  86.      */
  87.     public function exists(string|iterable $files): bool
  88.     {
  89.         $maxPathLength \PHP_MAXPATHLEN 2;
  90.         foreach ($this->toIterable($files) as $file) {
  91.             if (\strlen($file) > $maxPathLength) {
  92.                 throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.'$maxPathLength), 0null$file);
  93.             }
  94.             if (!file_exists($file)) {
  95.                 return false;
  96.             }
  97.         }
  98.         return true;
  99.     }
  100.     /**
  101.      * Sets access and modification time of file.
  102.      *
  103.      * @param int|null $time  The touch time as a Unix timestamp, if not supplied the current system time is used
  104.      * @param int|null $atime The access time as a Unix timestamp, if not supplied the current system time is used
  105.      *
  106.      * @throws IOException When touch fails
  107.      */
  108.     public function touch(string|iterable $filesint $time nullint $atime null)
  109.     {
  110.         foreach ($this->toIterable($files) as $file) {
  111.             if (!($time self::box('touch'$file$time$atime) : self::box('touch'$file))) {
  112.                 throw new IOException(sprintf('Failed to touch "%s": '$file).self::$lastError0null$file);
  113.             }
  114.         }
  115.     }
  116.     /**
  117.      * Removes files or directories.
  118.      *
  119.      * @throws IOException When removal fails
  120.      */
  121.     public function remove(string|iterable $files)
  122.     {
  123.         if ($files instanceof \Traversable) {
  124.             $files iterator_to_array($filesfalse);
  125.         } elseif (!\is_array($files)) {
  126.             $files = [$files];
  127.         }
  128.         self::doRemove($filesfalse);
  129.     }
  130.     private static function doRemove(array $filesbool $isRecursive): void
  131.     {
  132.         $files array_reverse($files);
  133.         foreach ($files as $file) {
  134.             if (is_link($file)) {
  135.                 // See https://bugs.php.net/52176
  136.                 if (!(self::box('unlink'$file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir'$file)) && file_exists($file)) {
  137.                     throw new IOException(sprintf('Failed to remove symlink "%s": '$file).self::$lastError);
  138.                 }
  139.             } elseif (is_dir($file)) {
  140.                 if (!$isRecursive) {
  141.                     $tmpName \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=''-.'));
  142.                     if (file_exists($tmpName)) {
  143.                         try {
  144.                             self::doRemove([$tmpName], true);
  145.                         } catch (IOException) {
  146.                         }
  147.                     }
  148.                     if (!file_exists($tmpName) && self::box('rename'$file$tmpName)) {
  149.                         $origFile $file;
  150.                         $file $tmpName;
  151.                     } else {
  152.                         $origFile null;
  153.                     }
  154.                 }
  155.                 $filesystemIterator = new \FilesystemIterator($file\FilesystemIterator::CURRENT_AS_PATHNAME \FilesystemIterator::SKIP_DOTS);
  156.                 self::doRemove(iterator_to_array($filesystemIteratortrue), true);
  157.                 if (!self::box('rmdir'$file) && file_exists($file) && !$isRecursive) {
  158.                     $lastError self::$lastError;
  159.                     if (null !== $origFile && self::box('rename'$file$origFile)) {
  160.                         $file $origFile;
  161.                     }
  162.                     throw new IOException(sprintf('Failed to remove directory "%s": '$file).$lastError);
  163.                 }
  164.             } elseif (!self::box('unlink'$file) && (str_contains(self::$lastError'Permission denied') || file_exists($file))) {
  165.                 throw new IOException(sprintf('Failed to remove file "%s": '$file).self::$lastError);
  166.             }
  167.         }
  168.     }
  169.     /**
  170.      * Change mode for an array of files or directories.
  171.      *
  172.      * @param int  $mode      The new mode (octal)
  173.      * @param int  $umask     The mode mask (octal)
  174.      * @param bool $recursive Whether change the mod recursively or not
  175.      *
  176.      * @throws IOException When the change fails
  177.      */
  178.     public function chmod(string|iterable $filesint $modeint $umask 0000bool $recursive false)
  179.     {
  180.         foreach ($this->toIterable($files) as $file) {
  181.             if (\is_int($mode) && !self::box('chmod'$file$mode & ~$umask)) {
  182.                 throw new IOException(sprintf('Failed to chmod file "%s": '$file).self::$lastError0null$file);
  183.             }
  184.             if ($recursive && is_dir($file) && !is_link($file)) {
  185.                 $this->chmod(new \FilesystemIterator($file), $mode$umasktrue);
  186.             }
  187.         }
  188.     }
  189.     /**
  190.      * Change the owner of an array of files or directories.
  191.      *
  192.      * @param string|int $user      A user name or number
  193.      * @param bool       $recursive Whether change the owner recursively or not
  194.      *
  195.      * @throws IOException When the change fails
  196.      */
  197.     public function chown(string|iterable $filesstring|int $userbool $recursive false)
  198.     {
  199.         foreach ($this->toIterable($files) as $file) {
  200.             if ($recursive && is_dir($file) && !is_link($file)) {
  201.                 $this->chown(new \FilesystemIterator($file), $usertrue);
  202.             }
  203.             if (is_link($file) && \function_exists('lchown')) {
  204.                 if (!self::box('lchown'$file$user)) {
  205.                     throw new IOException(sprintf('Failed to chown file "%s": '$file).self::$lastError0null$file);
  206.                 }
  207.             } else {
  208.                 if (!self::box('chown'$file$user)) {
  209.                     throw new IOException(sprintf('Failed to chown file "%s": '$file).self::$lastError0null$file);
  210.                 }
  211.             }
  212.         }
  213.     }
  214.     /**
  215.      * Change the group of an array of files or directories.
  216.      *
  217.      * @param string|int $group     A group name or number
  218.      * @param bool       $recursive Whether change the group recursively or not
  219.      *
  220.      * @throws IOException When the change fails
  221.      */
  222.     public function chgrp(string|iterable $filesstring|int $groupbool $recursive false)
  223.     {
  224.         foreach ($this->toIterable($files) as $file) {
  225.             if ($recursive && is_dir($file) && !is_link($file)) {
  226.                 $this->chgrp(new \FilesystemIterator($file), $grouptrue);
  227.             }
  228.             if (is_link($file) && \function_exists('lchgrp')) {
  229.                 if (!self::box('lchgrp'$file$group)) {
  230.                     throw new IOException(sprintf('Failed to chgrp file "%s": '$file).self::$lastError0null$file);
  231.                 }
  232.             } else {
  233.                 if (!self::box('chgrp'$file$group)) {
  234.                     throw new IOException(sprintf('Failed to chgrp file "%s": '$file).self::$lastError0null$file);
  235.                 }
  236.             }
  237.         }
  238.     }
  239.     /**
  240.      * Renames a file or a directory.
  241.      *
  242.      * @throws IOException When target file or directory already exists
  243.      * @throws IOException When origin cannot be renamed
  244.      */
  245.     public function rename(string $originstring $targetbool $overwrite false)
  246.     {
  247.         // we check that target does not exist
  248.         if (!$overwrite && $this->isReadable($target)) {
  249.             throw new IOException(sprintf('Cannot rename because the target "%s" already exists.'$target), 0null$target);
  250.         }
  251.         if (!self::box('rename'$origin$target)) {
  252.             if (is_dir($origin)) {
  253.                 // See https://bugs.php.net/54097 & https://php.net/rename#113943
  254.                 $this->mirror($origin$targetnull, ['override' => $overwrite'delete' => $overwrite]);
  255.                 $this->remove($origin);
  256.                 return;
  257.             }
  258.             throw new IOException(sprintf('Cannot rename "%s" to "%s": '$origin$target).self::$lastError0null$target);
  259.         }
  260.     }
  261.     /**
  262.      * Tells whether a file exists and is readable.
  263.      *
  264.      * @throws IOException When windows path is longer than 258 characters
  265.      */
  266.     private function isReadable(string $filename): bool
  267.     {
  268.         $maxPathLength \PHP_MAXPATHLEN 2;
  269.         if (\strlen($filename) > $maxPathLength) {
  270.             throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.'$maxPathLength), 0null$filename);
  271.         }
  272.         return is_readable($filename);
  273.     }
  274.     /**
  275.      * Creates a symbolic link or copy a directory.
  276.      *
  277.      * @throws IOException When symlink fails
  278.      */
  279.     public function symlink(string $originDirstring $targetDirbool $copyOnWindows false)
  280.     {
  281.         self::assertFunctionExists('symlink');
  282.         if ('\\' === \DIRECTORY_SEPARATOR) {
  283.             $originDir strtr($originDir'/''\\');
  284.             $targetDir strtr($targetDir'/''\\');
  285.             if ($copyOnWindows) {
  286.                 $this->mirror($originDir$targetDir);
  287.                 return;
  288.             }
  289.         }
  290.         $this->mkdir(\dirname($targetDir));
  291.         if (is_link($targetDir)) {
  292.             if (readlink($targetDir) === $originDir) {
  293.                 return;
  294.             }
  295.             $this->remove($targetDir);
  296.         }
  297.         if (!self::box('symlink'$originDir$targetDir)) {
  298.             $this->linkException($originDir$targetDir'symbolic');
  299.         }
  300.     }
  301.     /**
  302.      * Creates a hard link, or several hard links to a file.
  303.      *
  304.      * @param string|string[] $targetFiles The target file(s)
  305.      *
  306.      * @throws FileNotFoundException When original file is missing or not a file
  307.      * @throws IOException           When link fails, including if link already exists
  308.      */
  309.     public function hardlink(string $originFilestring|iterable $targetFiles)
  310.     {
  311.         self::assertFunctionExists('link');
  312.         if (!$this->exists($originFile)) {
  313.             throw new FileNotFoundException(null0null$originFile);
  314.         }
  315.         if (!is_file($originFile)) {
  316.             throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.'$originFile));
  317.         }
  318.         foreach ($this->toIterable($targetFiles) as $targetFile) {
  319.             if (is_file($targetFile)) {
  320.                 if (fileinode($originFile) === fileinode($targetFile)) {
  321.                     continue;
  322.                 }
  323.                 $this->remove($targetFile);
  324.             }
  325.             if (!self::box('link'$originFile$targetFile)) {
  326.                 $this->linkException($originFile$targetFile'hard');
  327.             }
  328.         }
  329.     }
  330.     /**
  331.      * @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
  332.      */
  333.     private function linkException(string $originstring $targetstring $linkType)
  334.     {
  335.         if (self::$lastError) {
  336.             if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError'error code(1314)')) {
  337.                 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), 0null$target);
  338.             }
  339.         }
  340.         throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": '$linkType$origin$target).self::$lastError0null$target);
  341.     }
  342.     /**
  343.      * Resolves links in paths.
  344.      *
  345.      * With $canonicalize = false (default)
  346.      *      - if $path does not exist or is not a link, returns null
  347.      *      - if $path is a link, returns the next direct target of the link without considering the existence of the target
  348.      *
  349.      * With $canonicalize = true
  350.      *      - if $path does not exist, returns null
  351.      *      - if $path exists, returns its absolute fully resolved final version
  352.      */
  353.     public function readlink(string $pathbool $canonicalize false): ?string
  354.     {
  355.         if (!$canonicalize && !is_link($path)) {
  356.             return null;
  357.         }
  358.         if ($canonicalize) {
  359.             if (!$this->exists($path)) {
  360.                 return null;
  361.             }
  362.             return realpath($path);
  363.         }
  364.         return readlink($path);
  365.     }
  366.     /**
  367.      * Given an existing path, convert it to a path relative to a given starting path.
  368.      */
  369.     public function makePathRelative(string $endPathstring $startPath): string
  370.     {
  371.         if (!$this->isAbsolutePath($startPath)) {
  372.             throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.'$startPath));
  373.         }
  374.         if (!$this->isAbsolutePath($endPath)) {
  375.             throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.'$endPath));
  376.         }
  377.         // Normalize separators on Windows
  378.         if ('\\' === \DIRECTORY_SEPARATOR) {
  379.             $endPath str_replace('\\''/'$endPath);
  380.             $startPath str_replace('\\''/'$startPath);
  381.         }
  382.         $splitDriveLetter = function ($path) {
  383.             return (\strlen($path) > && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
  384.                 ? [substr($path2), strtoupper($path[0])]
  385.                 : [$pathnull];
  386.         };
  387.         $splitPath = function ($path) {
  388.             $result = [];
  389.             foreach (explode('/'trim($path'/')) as $segment) {
  390.                 if ('..' === $segment) {
  391.                     array_pop($result);
  392.                 } elseif ('.' !== $segment && '' !== $segment) {
  393.                     $result[] = $segment;
  394.                 }
  395.             }
  396.             return $result;
  397.         };
  398.         [$endPath$endDriveLetter] = $splitDriveLetter($endPath);
  399.         [$startPath$startDriveLetter] = $splitDriveLetter($startPath);
  400.         $startPathArr $splitPath($startPath);
  401.         $endPathArr $splitPath($endPath);
  402.         if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
  403.             // End path is on another drive, so no relative path exists
  404.             return $endDriveLetter.':/'.($endPathArr implode('/'$endPathArr).'/' '');
  405.         }
  406.         // Find for which directory the common path stops
  407.         $index 0;
  408.         while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
  409.             ++$index;
  410.         }
  411.         // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
  412.         if (=== \count($startPathArr) && '' === $startPathArr[0]) {
  413.             $depth 0;
  414.         } else {
  415.             $depth \count($startPathArr) - $index;
  416.         }
  417.         // Repeated "../" for each level need to reach the common path
  418.         $traverser str_repeat('../'$depth);
  419.         $endPathRemainder implode('/'\array_slice($endPathArr$index));
  420.         // Construct $endPath from traversing to the common path, then to the remaining $endPath
  421.         $relativePath $traverser.('' !== $endPathRemainder $endPathRemainder.'/' '');
  422.         return '' === $relativePath './' $relativePath;
  423.     }
  424.     /**
  425.      * Mirrors a directory to another.
  426.      *
  427.      * Copies files and directories from the origin directory into the target directory. By default:
  428.      *
  429.      *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
  430.      *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
  431.      *
  432.      * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created
  433.      * @param array             $options  An array of boolean options
  434.      *                                    Valid options are:
  435.      *                                    - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
  436.      *                                    - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
  437.      *                                    - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
  438.      *
  439.      * @throws IOException When file type is unknown
  440.      */
  441.     public function mirror(string $originDirstring $targetDir\Traversable $iterator null, array $options = [])
  442.     {
  443.         $targetDir rtrim($targetDir'/\\');
  444.         $originDir rtrim($originDir'/\\');
  445.         $originDirLen \strlen($originDir);
  446.         if (!$this->exists($originDir)) {
  447.             throw new IOException(sprintf('The origin directory specified "%s" was not found.'$originDir), 0null$originDir);
  448.         }
  449.         // Iterate in destination folder to remove obsolete entries
  450.         if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
  451.             $deleteIterator $iterator;
  452.             if (null === $deleteIterator) {
  453.                 $flags \FilesystemIterator::SKIP_DOTS;
  454.                 $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir$flags), \RecursiveIteratorIterator::CHILD_FIRST);
  455.             }
  456.             $targetDirLen \strlen($targetDir);
  457.             foreach ($deleteIterator as $file) {
  458.                 $origin $originDir.substr($file->getPathname(), $targetDirLen);
  459.                 if (!$this->exists($origin)) {
  460.                     $this->remove($file);
  461.                 }
  462.             }
  463.         }
  464.         $copyOnWindows $options['copy_on_windows'] ?? false;
  465.         if (null === $iterator) {
  466.             $flags $copyOnWindows \FilesystemIterator::SKIP_DOTS \FilesystemIterator::FOLLOW_SYMLINKS \FilesystemIterator::SKIP_DOTS;
  467.             $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir$flags), \RecursiveIteratorIterator::SELF_FIRST);
  468.         }
  469.         $this->mkdir($targetDir);
  470.         $filesCreatedWhileMirroring = [];
  471.         foreach ($iterator as $file) {
  472.             if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) {
  473.                 continue;
  474.             }
  475.             $target $targetDir.substr($file->getPathname(), $originDirLen);
  476.             $filesCreatedWhileMirroring[$target] = true;
  477.             if (!$copyOnWindows && is_link($file)) {
  478.                 $this->symlink($file->getLinkTarget(), $target);
  479.             } elseif (is_dir($file)) {
  480.                 $this->mkdir($target);
  481.             } elseif (is_file($file)) {
  482.                 $this->copy($file$target$options['override'] ?? false);
  483.             } else {
  484.                 throw new IOException(sprintf('Unable to guess "%s" file type.'$file), 0null$file);
  485.             }
  486.         }
  487.     }
  488.     /**
  489.      * Returns whether the file path is an absolute path.
  490.      */
  491.     public function isAbsolutePath(string $file): bool
  492.     {
  493.         return '' !== $file && (strspn($file'/\\'01)
  494.             || (\strlen($file) > && ctype_alpha($file[0])
  495.                 && ':' === $file[1]
  496.                 && strspn($file'/\\'21)
  497.             )
  498.             || null !== parse_url($file\PHP_URL_SCHEME)
  499.         );
  500.     }
  501.     /**
  502.      * Creates a temporary file with support for custom stream wrappers.
  503.      *
  504.      * @param string $prefix The prefix of the generated temporary filename
  505.      *                       Note: Windows uses only the first three characters of prefix
  506.      * @param string $suffix The suffix of the generated temporary filename
  507.      *
  508.      * @return string The new temporary filename (with path), or throw an exception on failure
  509.      */
  510.     public function tempnam(string $dirstring $prefixstring $suffix ''): string
  511.     {
  512.         [$scheme$hierarchy] = $this->getSchemeAndHierarchy($dir);
  513.         // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
  514.         if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) {
  515.             // If tempnam failed or no scheme return the filename otherwise prepend the scheme
  516.             if ($tmpFile self::box('tempnam'$hierarchy$prefix)) {
  517.                 if (null !== $scheme && 'gs' !== $scheme) {
  518.                     return $scheme.'://'.$tmpFile;
  519.                 }
  520.                 return $tmpFile;
  521.             }
  522.             throw new IOException('A temporary file could not be created: '.self::$lastError);
  523.         }
  524.         // Loop until we create a valid temp file or have reached 10 attempts
  525.         for ($i 0$i 10; ++$i) {
  526.             // Create a unique filename
  527.             $tmpFile $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix;
  528.             // Use fopen instead of file_exists as some streams do not support stat
  529.             // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
  530.             if (!$handle self::box('fopen'$tmpFile'x+')) {
  531.                 continue;
  532.             }
  533.             // Close the file if it was successfully opened
  534.             self::box('fclose'$handle);
  535.             return $tmpFile;
  536.         }
  537.         throw new IOException('A temporary file could not be created: '.self::$lastError);
  538.     }
  539.     /**
  540.      * Atomically dumps content into a file.
  541.      *
  542.      * @param string|resource $content The data to write into the file
  543.      *
  544.      * @throws IOException if the file cannot be written to
  545.      */
  546.     public function dumpFile(string $filename$content)
  547.     {
  548.         if (\is_array($content)) {
  549.             throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.'__METHOD__));
  550.         }
  551.         $dir \dirname($filename);
  552.         if (!is_dir($dir)) {
  553.             $this->mkdir($dir);
  554.         }
  555.         // Will create a temp file with 0600 access rights
  556.         // when the filesystem supports chmod.
  557.         $tmpFile $this->tempnam($dirbasename($filename));
  558.         try {
  559.             if (false === self::box('file_put_contents'$tmpFile$content)) {
  560.                 throw new IOException(sprintf('Failed to write file "%s": '$filename).self::$lastError0null$filename);
  561.             }
  562.             self::box('chmod'$tmpFilefile_exists($filename) ? fileperms($filename) : 0666 & ~umask());
  563.             $this->rename($tmpFile$filenametrue);
  564.         } finally {
  565.             if (file_exists($tmpFile)) {
  566.                 self::box('unlink'$tmpFile);
  567.             }
  568.         }
  569.     }
  570.     /**
  571.      * Appends content to an existing file.
  572.      *
  573.      * @param string|resource $content The content to append
  574.      * @param bool            $lock    Whether the file should be locked when writing to it
  575.      *
  576.      * @throws IOException If the file is not writable
  577.      */
  578.     public function appendToFile(string $filename$content/* , bool $lock = false */)
  579.     {
  580.         if (\is_array($content)) {
  581.             throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.'__METHOD__));
  582.         }
  583.         $dir \dirname($filename);
  584.         if (!is_dir($dir)) {
  585.             $this->mkdir($dir);
  586.         }
  587.         $lock \func_num_args() > && func_get_arg(2);
  588.         if (false === self::box('file_put_contents'$filename$content\FILE_APPEND | ($lock \LOCK_EX 0))) {
  589.             throw new IOException(sprintf('Failed to write file "%s": '$filename).self::$lastError0null$filename);
  590.         }
  591.     }
  592.     private function toIterable(string|iterable $files): iterable
  593.     {
  594.         return is_iterable($files) ? $files : [$files];
  595.     }
  596.     /**
  597.      * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
  598.      */
  599.     private function getSchemeAndHierarchy(string $filename): array
  600.     {
  601.         $components explode('://'$filename2);
  602.         return === \count($components) ? [$components[0], $components[1]] : [null$components[0]];
  603.     }
  604.     private static function assertFunctionExists(string $func): void
  605.     {
  606.         if (!\function_exists($func)) {
  607.             throw new IOException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.'$func));
  608.         }
  609.     }
  610.     private static function box(string $funcmixed ...$args): mixed
  611.     {
  612.         self::assertFunctionExists($func);
  613.         self::$lastError null;
  614.         set_error_handler(__CLASS__.'::handleError');
  615.         try {
  616.             return $func(...$args);
  617.         } finally {
  618.             restore_error_handler();
  619.         }
  620.     }
  621.     /**
  622.      * @internal
  623.      */
  624.     public static function handleError(int $typestring $msg)
  625.     {
  626.         self::$lastError $msg;
  627.     }
  628. }