Repository: humbug/php-scoper (php)

Issue:
scoper should copy file permissions
## Bug report

| Question    | Answer
| ------------| ---------------
| PHP-Scoper version | 0.14.0 
| PHP version | 7.4.16
| Platform with version | Alpine Linux 3.13.2

Scoper does not preserve file permissions when copying. I observed this for script files which are used in composer config:

```console
$ ls -lh bin
-rwxrwxr-x    1 root     root        2.8K Mar 15 20:31 post-install-update.sh
-rwxrwxr-x    1 root     root        1.2K Mar 15 20:31 scoper-fix-autoloader.php
$ php-scoper add-prefix
    ____  __  ______     _____
   / __ \/ / / / __ \   / ___/_________  ____  ___  _____
  / /_/ / /_/ / /_/ /   \__ \/ ___/ __ \/ __ \/ _ \/ ___/
 / ____/ __  / ____/   ___/ / /__/ /_/ / /_/ /  __/ /
/_/   /_/ /_/_/       /____/\___/\____/ .___/\___/_/
                                     /_/

PHP Scoper version 0.14.0 2020-12-21 14:36:07 UTC

 3215/3215 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

                                                                                                                        
 [OK] Successfully prefixed 3215 files.                                                                                 
                                                                                                                        

 // Memory usage: 31.77MB (peak: 49.64MB), time: 14.78s                                                                 


$ ls -lh build/bin/
total 8K     
-rw-r--r--    1 root     root        2.8K Mar 15 20:49 post-install-update.sh
-rw-r--r--    1 root     root        1.2K Mar 15 20:49 scoper-fix-autoloader.php
```


Pull request:
Preserve the files permissions
Closes #463

Patch:
diff --git a/fixtures/set002/original/executable-file.php b/fixtures/set002/original/executable-file.php
new file mode 100755
index 0000000..174d7fd
--- /dev/null
+++ b/fixtures/set002/original/executable-file.php
@@ -0,0 +1,3 @@
+<?php
+
+declare(strict_types=1);
diff --git a/fixtures/set002/scoped/executable-file.php b/fixtures/set002/scoped/executable-file.php
new file mode 100755
index 0000000..f12df89
--- /dev/null
+++ b/fixtures/set002/scoped/executable-file.php
@@ -0,0 +1,3 @@
+<?php
+
+declare (strict_types=1);
diff --git a/src/Console/ConsoleScoper.php b/src/Console/ConsoleScoper.php
index 3c2dce4..726e641 100644
--- a/src/Console/ConsoleScoper.php
+++ b/src/Console/ConsoleScoper.php
@@ -31,6 +31,7 @@ use function count;
 use function Humbug\PhpScoper\get_common_path;
 use function preg_match as native_preg_match;
 use function Safe\file_get_contents;
+use function Safe\fileperms;
 use function sprintf;
 use function str_replace;
 use function strlen;
@@ -99,7 +100,7 @@ final class ConsoleScoper
         // Creates output directory if does not already exist
         $this->fileSystem->mkdir($outputDir);
 
-        [$files, $whitelistedFiles] = self::getFiles($config, $outputDir);
+        [$files, $excludedFilesWithContents] = self::getFiles($config, $outputDir);
 
         $logger->outputFileCount(count($files));
 
@@ -121,14 +122,14 @@ final class ConsoleScoper
             );
         }
 
-        foreach ($whitelistedFiles as [$inputFilePath, $inputContents, $outputFilePath]) {
-            $this->fileSystem->dumpFile($outputFilePath, $inputContents);
+        foreach ($excludedFilesWithContents as $excludedFileWithContent) {
+            $this->dumpFileWithPermissions(...$excludedFileWithContent);
         }
 
         $vendorDir = self::findVendorDir(
             [
                 ...array_column($files, 2),
-                ...array_column($whitelistedFiles, 2),
+                ...array_column($excludedFilesWithContents, 2),
             ],
         );
 
@@ -142,6 +143,21 @@ final class ConsoleScoper
         }
     }
 
+    private function dumpFileWithPermissions(
+        string $inputFilePath,
+        string $inputContents,
+        string $outputFilePath
+    ): void {
+        $this->fileSystem->dumpFile($outputFilePath, $inputContents);
+
+        $originalFilePermissions = fileperms($inputFilePath) & 0o777;
+
+        if ($originalFilePermissions !== 420) {
+            // Only change the permissions if necessary
+            $this->fileSystem->chmod($outputFilePath, $originalFilePermissions);
+        }
+    }
+
     /**
      * @return array{array<array{string, string, string}>, array<array{string, string, string}>}
      */
@@ -228,7 +244,11 @@ final class ConsoleScoper
             $scoppedContent = file_get_contents($inputFilePath);
         }
 
-        $this->fileSystem->dumpFile($outputFilePath, $scoppedContent);
+        $this->dumpFileWithPermissions(
+            $inputFilePath,
+            $scoppedContent,
+            $outputFilePath,
+        );
 
         if (!isset($exception)) {
             $logger->outputSuccess($inputFilePath);


Test patch:
diff --git a/tests/Console/Command/AddPrefixCommandIntegrationTest.php b/tests/Console/Command/AddPrefixCommandIntegrationTest.php
index ddc10e4..92bcd3d 100644
--- a/tests/Console/Command/AddPrefixCommandIntegrationTest.php
+++ b/tests/Console/Command/AddPrefixCommandIntegrationTest.php
@@ -26,6 +26,7 @@ use function array_reduce;
 use function iterator_to_array;
 use function Safe\file_get_contents;
 use function Safe\file_put_contents;
+use function Safe\fileperms;
 use function Safe\preg_replace;
 use function Safe\realpath;
 use function sprintf;
@@ -36,7 +37,6 @@ use const DIRECTORY_SEPARATOR;
  * @coversNothing
  *
  * @group integration
- * @runTestsInSeparateProcesses
  *
  * @internal
  */
@@ -135,10 +135,10 @@ class AddPrefixCommandIntegrationTest extends FileSystemTestCase implements AppT
 
             PhpScoper version TestVersion 28/01/2020
 
-             0/4 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%
-             4/4 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
+             0/5 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%
+             5/5 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 
-             [OK] Successfully prefixed 4 files.
+             [OK] Successfully prefixed 5 files.
 
              // Memory usage: 5.00MB (peak: 10.00MB), time: 0.00s
 
@@ -196,12 +196,13 @@ class AddPrefixCommandIntegrationTest extends FileSystemTestCase implements AppT
             PhpScoper version TestVersion 28/01/2020
 
              * [NO] /path/to/composer/installed.json
+             * [OK] /path/to/executable-file.php
              * [OK] /path/to/file.php
              * [NO] /path/to/invalid-file.php
              * [OK] /path/to/scoper.inc.php
 
 
-             [OK] Successfully prefixed 4 files.
+             [OK] Successfully prefixed 5 files.
 
              // Memory usage: 5.00MB (peak: 10.00MB), time: 0.00s
 
@@ -257,6 +258,7 @@ class AddPrefixCommandIntegrationTest extends FileSystemTestCase implements AppT
             #7
             #8
             #9
+             * [OK] /path/to/executable-file.php
              * [OK] /path/to/file.php
              * [NO] /path/to/invalid-file.php
             	Could not parse the file "/path/to/invalid-file.php".: PhpParser
@@ -274,7 +276,7 @@ class AddPrefixCommandIntegrationTest extends FileSystemTestCase implements AppT
              * [OK] /path/to/scoper.inc.php
 
 
-             [OK] Successfully prefixed 4 files.
+             [OK] Successfully prefixed 5 files.
 
              // Memory usage: 5.00MB (peak: 10.00MB), time: 0.00s
 
@@ -351,7 +353,7 @@ class AddPrefixCommandIntegrationTest extends FileSystemTestCase implements AppT
 
                 $path = str_replace($dir, '', $realPath);
 
-                $collectedFiles[$path] = file_get_contents($realPath);
+                $collectedFiles[$path] = [file_get_contents($realPath), fileperms($realPath)];
 
                 return $collectedFiles;
             },
diff --git a/tests/Console/Command/AddPrefixCommandTest.php b/tests/Console/Command/AddPrefixCommandTest.php
index f9bc150..73aa2a0 100644
--- a/tests/Console/Command/AddPrefixCommandTest.php
+++ b/tests/Console/Command/AddPrefixCommandTest.php
@@ -105,6 +105,7 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $expectedFiles = [
             'composer/installed.json' => 'f1',
+            'executable-file.php' => 'f5',
             'file.php' => 'f2',
             'invalid-file.php' => 'f3',
             'scoper.inc.php' => 'f4',
@@ -112,6 +113,13 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $root = realpath($root);
 
+        $this->fileSystemProphecy
+            ->chmod(
+                $this->tmp.'/executable-file.php',
+                493,
+            )
+            ->shouldBeCalled();
+
         foreach ($expectedFiles as $expectedFile => $prefixedContents) {
             $inputPath = escape_path($root.'/'.$expectedFile);
             $outputPath = escape_path($this->tmp.'/'.$expectedFile);
@@ -159,6 +167,7 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $expectedFiles = [
             'composer/installed.json' => 'f1',
+            'executable-file.php' => 'f5',
             'file.php' => 'f2',
             'invalid-file.php' => 'f3',
             'scoper.inc.php' => null,
@@ -166,6 +175,13 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $root = realpath($root);
 
+        $this->fileSystemProphecy
+            ->chmod(
+                $this->tmp.'/executable-file.php',
+                493,
+            )
+            ->shouldBeCalled();
+
         foreach ($expectedFiles as $expectedFile => $prefixedContents) {
             $inputPath = escape_path($root.'/'.$expectedFile);
             $outputPath = escape_path($this->tmp.'/'.$expectedFile);
@@ -315,6 +331,7 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $expectedFiles = [
             'composer/installed.json' => 'f1',
+            'executable-file.php' => 'f5',
             'file.php' => 'f2',
             'invalid-file.php' => 'f3',
             'scoper.inc.php' => 'f4',
@@ -322,6 +339,13 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $root = realpath($root);
 
+        $this->fileSystemProphecy
+            ->chmod(
+                $this->tmp.'/executable-file.php',
+                493,
+            )
+            ->shouldBeCalled();
+
         foreach ($expectedFiles as $expectedFile => $prefixedContents) {
             $inputPath = escape_path($root.'/'.$expectedFile);
             $outputPath = escape_path($this->tmp.'/'.$expectedFile);
@@ -365,6 +389,7 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $expectedFiles = [
             'composer/installed.json' => 'f1',
+            'executable-file.php' => 'f5',
             'file.php' => 'f2',
             'invalid-file.php' => 'f3',
             'scoper.inc.php' => 'f4',
@@ -372,6 +397,13 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $root = realpath($root);
 
+        $this->fileSystemProphecy
+            ->chmod(
+                $outDir.'/executable-file.php',
+                493,
+            )
+            ->shouldBeCalled();
+
         foreach ($expectedFiles as $expectedFile => $prefixedContents) {
             $inputPath = escape_path($root.'/'.$expectedFile);
             $outputPath = escape_path($outDir.'/'.$expectedFile);
@@ -416,6 +448,7 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $expectedFiles = [
             'composer/installed.json' => 'f1',
+            'executable-file.php' => 'f5',
             'file.php' => 'f2',
             'invalid-file.php' => 'f3',
             'scoper.inc.php' => 'f4',
@@ -423,6 +456,13 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
 
         $root = realpath($root);
 
+        $this->fileSystemProphecy
+            ->chmod(
+                $this->tmp.DIRECTORY_SEPARATOR.$outDir.'/executable-file.php',
+                493,
+            )
+            ->shouldBeCalled();
+
         foreach ($expectedFiles as $expectedFile => $prefixedContents) {
             $inputPath = escape_path($root.'/'.$expectedFile);
             $outputPath = escape_path($this->tmp.DIRECTORY_SEPARATOR.$outDir.'/'.$expectedFile);
@@ -526,6 +566,7 @@ class AddPrefixCommandTest extends FileSystemTestCase implements AppTesterTestCa
         $this->fileSystemProphecy->mkdir($this->tmp)->shouldBeCalled();
         $this->fileSystemProphecy->exists(Argument::cetera())->willReturn(false);
         $this->fileSystemProphecy->remove(Argument::cetera())->shouldNotBeCalled();
+        $this->fileSystemProphecy->chmod(Argument::cetera())->shouldNotBeCalled();
 
         $expectedFiles = [
             'invalid-json.json' => 'f1',