vendor/symfony/filesystem/Filesystem.php line 135

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