diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index 079d34c54..aff80e4cd 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -16,6 +16,7 @@ use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\LoaderInterface; +use Composer\Util\Zip; /** * @author Serge Smertin @@ -80,76 +81,15 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito } } - /** - * Find a file by name, returning the one that has the shortest path. - * - * @param \ZipArchive $zip - * @param string $filename - * @return bool|int - */ - private function locateFile(\ZipArchive $zip, $filename) - { - $indexOfShortestMatch = false; - $lengthOfShortestMatch = -1; - - for ($i = 0; $i < $zip->numFiles; $i++) { - $stat = $zip->statIndex($i); - if (strcmp(basename($stat['name']), $filename) === 0) { - $directoryName = dirname($stat['name']); - if ($directoryName == '.') { - //if composer.json is in root directory - //it has to be the one to use. - return $i; - } - - if (strpos($directoryName, '\\') !== false || - strpos($directoryName, '/') !== false) { - //composer.json files below first directory are rejected - continue; - } - - $length = strlen($stat['name']); - if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) { - //Check it's not a directory. - $contents = $zip->getFromIndex($i); - if ($contents !== false) { - $indexOfShortestMatch = $i; - $lengthOfShortestMatch = $length; - } - } - } - } - - return $indexOfShortestMatch; - } - private function getComposerInformation(\SplFileInfo $file) { - $zip = new \ZipArchive(); - if ($zip->open($file->getPathname()) !== true) { + $json = Zip::getComposerJson($file->getPathname()); + + if (null === $json) { return false; } - if (0 == $zip->numFiles) { - $zip->close(); - - return false; - } - - $foundFileIndex = $this->locateFile($zip, 'composer.json'); - if (false === $foundFileIndex) { - $zip->close(); - - return false; - } - - $configurationFileName = $zip->getNameIndex($foundFileIndex); - $zip->close(); - - $composerFile = "zip://{$file->getPathname()}#$configurationFileName"; - $json = file_get_contents($composerFile); - - $package = JsonFile::parseJson($json, $composerFile); + $package = JsonFile::parseJson($json, $file->getPathname().'#composer.json'); $package['dist'] = array( 'type' => 'zip', 'url' => strtr($file->getPathname(), '\\', '/'), diff --git a/src/Composer/Util/Zip.php b/src/Composer/Util/Zip.php new file mode 100644 index 000000000..8c79d106c --- /dev/null +++ b/src/Composer/Util/Zip.php @@ -0,0 +1,108 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * @author Andreas Schempp + */ +class Zip +{ + /** + * Gets content of the root composer.json inside a ZIP archive. + * + * @param string $pathToZip + * @param string $filename + * + * @return string|null + */ + public static function getComposerJson($pathToZip) + { + if (!extension_loaded('zip')) { + throw new \RuntimeException('The Zip Util requires PHP\'s zip extension'); + } + + $zip = new \ZipArchive(); + if ($zip->open($pathToZip) !== true) { + return null; + } + + if (0 == $zip->numFiles) { + $zip->close(); + + return null; + } + + $foundFileIndex = self::locateFile($zip, 'composer.json'); + if (false === $foundFileIndex) { + $zip->close(); + + return null; + } + + $content = null; + $configurationFileName = $zip->getNameIndex($foundFileIndex); + $stream = $zip->getStream($configurationFileName); + + if (false !== $stream) { + $content = stream_get_contents($stream); + } + + $zip->close(); + + return $content; + } + + /** + * Find a file by name, returning the one that has the shortest path. + * + * @param \ZipArchive $zip + * @param string $filename + * + * @return bool|int + */ + private static function locateFile(\ZipArchive $zip, $filename) + { + $indexOfShortestMatch = false; + $lengthOfShortestMatch = -1; + + for ($i = 0; $i < $zip->numFiles; $i++) { + $stat = $zip->statIndex($i); + if (strcmp(basename($stat['name']), $filename) === 0) { + $directoryName = dirname($stat['name']); + if ($directoryName === '.') { + //if composer.json is in root directory + //it has to be the one to use. + return $i; + } + + if (strpos($directoryName, '\\') !== false || + strpos($directoryName, '/') !== false) { + //composer.json files below first directory are rejected + continue; + } + + $length = strlen($stat['name']); + if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) { + //Check it's not a directory. + $contents = $zip->getFromIndex($i); + if ($contents !== false) { + $indexOfShortestMatch = $i; + $lengthOfShortestMatch = $length; + } + } + } + } + + return $indexOfShortestMatch; + } +} diff --git a/tests/Composer/Test/Util/Fixtures/Zip/empty.zip b/tests/Composer/Test/Util/Fixtures/Zip/empty.zip new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/empty.zip differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/folder.zip b/tests/Composer/Test/Util/Fixtures/Zip/folder.zip new file mode 100644 index 000000000..72b17b542 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/folder.zip differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip b/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip new file mode 100644 index 000000000..db8c50302 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/multiple.zip differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/nojson.zip b/tests/Composer/Test/Util/Fixtures/Zip/nojson.zip new file mode 100644 index 000000000..e536b956c Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/nojson.zip differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/root.zip b/tests/Composer/Test/Util/Fixtures/Zip/root.zip new file mode 100644 index 000000000..fd08f4d34 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/root.zip differ diff --git a/tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip b/tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip new file mode 100644 index 000000000..93060bea2 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip differ diff --git a/tests/Composer/Test/Util/ZipTest.php b/tests/Composer/Test/Util/ZipTest.php new file mode 100644 index 000000000..c47373274 --- /dev/null +++ b/tests/Composer/Test/Util/ZipTest.php @@ -0,0 +1,117 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\Zip; +use PHPUnit\Framework\TestCase; + +/** + * @author Andreas Schempp + */ +class ZipTest extends TestCase +{ + public function testThrowsExceptionIfZipExcentionIsNotLoaded() + { + if (extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is loaded.'); + } + + $this->setExpectedException('\RuntimeException', 'The Zip Util requires PHP\'s zip extension'); + + Zip::getComposerJson(''); + } + + public function testReturnsNullifTheZipIsNotFound() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/invalid.zip'); + + $this->assertNull($result); + } + + public function testReturnsNullIfTheZipIsEmpty() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/empty.zip'); + + $this->assertNull($result); + } + + public function testReturnsNullIfTheZipHasNoComposerJson() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/nojson.zip'); + + $this->assertNull($result); + } + + public function testReturnsNullIfTheComposerJsonIsInASubSubfolder() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/subfolder.zip'); + + $this->assertNull($result); + } + + public function testReturnsComposerJsonInZipRoot() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/root.zip'); + + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } + + public function testReturnsComposerJsonInFirstFolder() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/folder.zip'); + + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } + + public function testReturnsRootComposerJsonAndSkipsSubfolders() + { + if (!extension_loaded('zip')) { + $this->markTestSkipped('The PHP zip extension is not loaded.'); + return; + } + + $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple.zip'); + + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } +}