1
0
Fork 0

Merge pull request #9058 from Seldaek/zip-cleanup

Clean up Zip Util to be more strict about what is a valid package archive
pull/9069/head
Jordi Boggiano 2020-07-21 17:17:12 +02:00 committed by GitHub
commit d8fa746433
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 71 additions and 37 deletions

View File

@ -88,7 +88,12 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito
private function getComposerInformation(\SplFileInfo $file) private function getComposerInformation(\SplFileInfo $file)
{ {
$json = Zip::getComposerJson($file->getPathname()); $json = null;
try {
$json = Zip::getComposerJson($file->getPathname());
} catch (\Exception $exception) {
$this->io->write('Failed loading package '.$file->getPathname().': '.$exception->getMessage(), false, IOInterface::VERBOSE);
}
if (null === $json) { if (null === $json) {
return false; return false;

View File

@ -66,42 +66,44 @@ class Zip
* *
* @param \ZipArchive $zip * @param \ZipArchive $zip
* @param string $filename * @param string $filename
* @throws \RuntimeException
* *
* @return bool|int * @return int
*/ */
private static function locateFile(\ZipArchive $zip, $filename) private static function locateFile(\ZipArchive $zip, $filename)
{ {
$indexOfShortestMatch = false; // return root composer.json if it is there and is a file
$lengthOfShortestMatch = -1; if (false !== ($index = $zip->locateName($filename)) && $zip->getFromIndex($index) !== false) {
return $index;
}
$topLevelPaths = array();
for ($i = 0; $i < $zip->numFiles; $i++) { for ($i = 0; $i < $zip->numFiles; $i++) {
$stat = $zip->statIndex($i); $name = $zip->getNameIndex($i);
if (strcmp(basename($stat['name']), $filename) === 0) { $dirname = dirname($name);
$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 || // handle archives with proper TOC
strpos($directoryName, '/') !== false) { if ($dirname === '.') {
//composer.json files below first directory are rejected $topLevelPaths[$name] = true;
continue; if (\count($topLevelPaths) > 1) {
throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths)));
} }
continue;
}
$length = strlen($stat['name']); // handle archives which do not have a TOC record for the directory itself
if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) { if (false === strpos('\\', $dirname) && false === strpos('/', $dirname)) {
//Check it's not a directory. $topLevelPaths[$dirname.'/'] = true;
$contents = $zip->getFromIndex($i); if (\count($topLevelPaths) > 1) {
if ($contents !== false) { throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths)));
$indexOfShortestMatch = $i;
$lengthOfShortestMatch = $length;
}
} }
} }
} }
return $indexOfShortestMatch; if ($topLevelPaths && false !== ($index = $zip->locateName(key($topLevelPaths).$filename)) && $zip->getFromIndex($index) !== false) {
return $index;
}
throw new \RuntimeException('No composer.json found either at the top level or within the topmost directory');
} }
} }

Binary file not shown.

Binary file not shown.

View File

@ -20,7 +20,7 @@ use Composer\Test\TestCase;
*/ */
class ZipTest extends TestCase class ZipTest extends TestCase
{ {
public function testThrowsExceptionIfZipExcentionIsNotLoaded() public function testThrowsExceptionIfZipExtensionIsNotLoaded()
{ {
if (extension_loaded('zip')) { if (extension_loaded('zip')) {
$this->markTestSkipped('The PHP zip extension is loaded.'); $this->markTestSkipped('The PHP zip extension is loaded.');
@ -55,28 +55,30 @@ class ZipTest extends TestCase
$this->assertNull($result); $this->assertNull($result);
} }
public function testReturnsNullIfTheZipHasNoComposerJson() /**
* @expectedException \RuntimeException
*/
public function testThrowsExceptionIfTheZipHasNoComposerJson()
{ {
if (!extension_loaded('zip')) { if (!extension_loaded('zip')) {
$this->markTestSkipped('The PHP zip extension is not loaded.'); $this->markTestSkipped('The PHP zip extension is not loaded.');
return; return;
} }
$result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/nojson.zip'); Zip::getComposerJson(__DIR__.'/Fixtures/Zip/nojson.zip');
$this->assertNull($result);
} }
public function testReturnsNullIfTheComposerJsonIsInASubSubfolder() /**
* @expectedException \RuntimeException
*/
public function testThrowsExceptionIfTheComposerJsonIsInASubSubfolder()
{ {
if (!extension_loaded('zip')) { if (!extension_loaded('zip')) {
$this->markTestSkipped('The PHP zip extension is not loaded.'); $this->markTestSkipped('The PHP zip extension is not loaded.');
return; return;
} }
$result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/subfolder.zip'); Zip::getComposerJson(__DIR__.'/Fixtures/Zip/subfolders.zip');
$this->assertNull($result);
} }
public function testReturnsComposerJsonInZipRoot() public function testReturnsComposerJsonInZipRoot()
@ -99,19 +101,44 @@ class ZipTest extends TestCase
} }
$result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/folder.zip'); $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/folder.zip');
$this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result);
} }
public function testReturnsRootComposerJsonAndSkipsSubfolders() /**
* @expectedException \RuntimeException
*/
public function testMultipleTopLevelDirsIsInvalid()
{ {
if (!extension_loaded('zip')) { if (!extension_loaded('zip')) {
$this->markTestSkipped('The PHP zip extension is not loaded.'); $this->markTestSkipped('The PHP zip extension is not loaded.');
return; return;
} }
$result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple.zip'); Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple.zip');
}
public function testReturnsComposerJsonFromFirstSubfolder()
{
if (!extension_loaded('zip')) {
$this->markTestSkipped('The PHP zip extension is not loaded.');
return;
}
$result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/single-sub.zip');
$this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result);
} }
/**
* @expectedException \RuntimeException
*/
public function testThrowsExceptionIfMultipleComposerInSubFoldersWereFound()
{
if (!extension_loaded('zip')) {
$this->markTestSkipped('The PHP zip extension is not loaded.');
return;
}
Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple_subfolders.zip');
}
} }