Browse Source

Add the hash of the parsed content of the composer.json to the lock file, and use it to verify the json is not changed

Zsolt Szeberenyi 9 years ago
parent
commit
50b560fe4c

+ 15 - 1
src/Composer/Factory.php

@@ -306,7 +306,7 @@ class Factory
             $lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION)
                 ? substr($composerFile, 0, -4).'lock'
                 : $composerFile . '.lock';
-            $locker = new Package\Locker($io, new JsonFile($lockFile, new RemoteFilesystem($io, $config)), $rm, $im, md5_file($composerFile));
+            $locker = new Package\Locker($io, new JsonFile($lockFile, new RemoteFilesystem($io, $config)), $rm, $im, md5_file($composerFile), $this->getContentHash($composerFile));
             $composer->setLocker($locker);
         }
 
@@ -485,4 +485,18 @@ class Factory
 
         return $factory->createComposer($io, $config, $disablePlugins);
     }
+
+    /**
+     * Returns the md5 hash of the sorted content of the composer file.
+     *
+     * @param string $composerFilePath Path to the composer file.
+     *
+     * @return string
+     */
+    private function getContentHash($composerFilePath)
+    {
+        $content = json_decode(file_get_contents($composerFilePath), true);
+        ksort($content);
+        return md5(json_encode($content));
+    }
 }

+ 10 - 1
src/Composer/Package/Locker.php

@@ -35,6 +35,7 @@ class Locker
     private $repositoryManager;
     private $installationManager;
     private $hash;
+    private $contentHash;
     private $loader;
     private $dumper;
     private $process;
@@ -48,13 +49,15 @@ class Locker
      * @param RepositoryManager   $repositoryManager   repository manager instance
      * @param InstallationManager $installationManager installation manager instance
      * @param string              $hash                unique hash of the current composer configuration
+     * @param string              $contentHash         unique hash of the content of the current composer configuration
      */
-    public function __construct(IOInterface $io, JsonFile $lockFile, RepositoryManager $repositoryManager, InstallationManager $installationManager, $hash)
+    public function __construct(IOInterface $io, JsonFile $lockFile, RepositoryManager $repositoryManager, InstallationManager $installationManager, $hash, $contentHash)
     {
         $this->lockFile = $lockFile;
         $this->repositoryManager = $repositoryManager;
         $this->installationManager = $installationManager;
         $this->hash = $hash;
+        $this->contentHash = $contentHash;
         $this->loader = new ArrayLoader(null, true);
         $this->dumper = new ArrayDumper();
         $this->process = new ProcessExecutor($io);
@@ -85,6 +88,11 @@ class Locker
     {
         $lock = $this->lockFile->read();
 
+        if (!empty($lock['content-hash'])) {
+            // There is a content hash key, use that instead of the file hash
+            return $this->contentHash == $lock['content-hash'];
+        }
+
         return $this->hash === $lock['hash'];
     }
 
@@ -241,6 +249,7 @@ class Locker
                                'Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file',
                                'This file is @gener'.'ated automatically'),
             'hash' => $this->hash,
+            'content-hash' => $this->contentHash,
             'packages' => null,
             'packages-dev' => null,
             'aliases' => array(),

+ 3 - 1
tests/Composer/Test/InstallerTest.php

@@ -191,7 +191,8 @@ class InstallerTest extends TestCase
                 }));
         }
 
-        $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), md5(json_encode($composerConfig)));
+        $hash   = md5(json_encode($composerConfig));
+        $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), $hash, $hash);
         $composer->setLocker($locker);
 
         $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();
@@ -237,6 +238,7 @@ class InstallerTest extends TestCase
 
         if ($expectLock) {
             unset($actualLock['hash']);
+            unset($actualLock['content-hash']);
             unset($actualLock['_readme']);
             $this->assertEquals($expectLock, $actualLock);
         }

+ 40 - 7
tests/Composer/Test/Package/LockerTest.php

@@ -20,7 +20,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase
     public function testIsLocked()
     {
         $json   = $this->createJsonFileMock();
-        $locker = new Locker(new NullIO, $json, $this->createRepositoryManagerMock(), $this->createInstallationManagerMock(), 'md5');
+        $locker = new Locker(new NullIO, $json, $this->createRepositoryManagerMock(), $this->createInstallationManagerMock(), 'md5', 'contentMd5');
 
         $json
             ->expects($this->any())
@@ -40,7 +40,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase
         $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5');
+        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5', 'contentMd5');
 
         $json
             ->expects($this->once())
@@ -58,7 +58,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase
         $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5');
+        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5', 'contentMd5');
 
         $json
             ->expects($this->once())
@@ -85,7 +85,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase
         $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5');
+        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5', 'contentMd5');
 
         $package1 = $this->createPackageMock();
         $package2 = $this->createPackageMock();
@@ -124,6 +124,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase
                                    'Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file',
                                    'This file is @gener'.'ated automatically'),
                 'hash' => 'md5',
+                'content-hash' => 'contentMd5',
                 'packages' => array(
                     array('name' => 'pkg1', 'version' => '1.0.0-beta'),
                     array('name' => 'pkg2', 'version' => '0.1.10')
@@ -148,7 +149,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase
         $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5');
+        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5', 'md5');
 
         $package1 = $this->createPackageMock();
         $package1
@@ -167,7 +168,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase
         $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5');
+        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5', 'contentMd5');
 
         $json
             ->expects($this->once())
@@ -183,7 +184,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase
         $repo = $this->createRepositoryManagerMock();
         $inst = $this->createInstallationManagerMock();
 
-        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5');
+        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5', 'contentMd5');
 
         $json
             ->expects($this->once())
@@ -193,6 +194,38 @@ class LockerTest extends \PHPUnit_Framework_TestCase
         $this->assertFalse($locker->isFresh());
     }
 
+    public function testIsFreshWithContentHash()
+    {
+        $json = $this->createJsonFileMock();
+        $repo = $this->createRepositoryManagerMock();
+        $inst = $this->createInstallationManagerMock();
+
+        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5', 'contentMd5');
+
+        $json
+            ->expects($this->once())
+            ->method('read')
+            ->will($this->returnValue(array('hash' => 'oldMd5', 'content-hash' => 'contentMd5')));
+
+        $this->assertTrue($locker->isFresh());
+    }
+
+    public function testIsFreshFalseWithContentHash()
+    {
+        $json = $this->createJsonFileMock();
+        $repo = $this->createRepositoryManagerMock();
+        $inst = $this->createInstallationManagerMock();
+
+        $locker = new Locker(new NullIO, $json, $repo, $inst, 'md5', 'contentMd5');
+
+        $json
+            ->expects($this->once())
+            ->method('read')
+            ->will($this->returnValue(array('hash' => 'md5', 'content-hash' => 'oldMd5')));
+
+        $this->assertFalse($locker->isFresh());
+    }
+
     private function createJsonFileMock()
     {
         return $this->getMockBuilder('Composer\Json\JsonFile')