diff --git a/doc/05-repositories.md b/doc/05-repositories.md index b32d92690..12a9dced0 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -516,7 +516,7 @@ There are some cases, when there is no ability to have one of the previously mentioned repository types online, even the VCS one. Typical example could be cross-organisation library exchange through built artifacts. Of course, most of the times they are private. To simplify maintenance, one can simply use a -repository of type `artifact` with a folder containing ZIP archives of those +repository of type `artifact` with a folder containing ZIP or TAR archives of those private packages: ```json diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index f8b068218..4f03fc7eb 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\Tar; use Composer\Util\Zip; /** @@ -66,7 +67,7 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito $directory = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS); $iterator = new \RecursiveIteratorIterator($directory); - $regex = new \RegexIterator($iterator, '/^.+\.(zip|phar)$/i'); + $regex = new \RegexIterator($iterator, '/^.+\.(zip|phar|tar|gz|tgz)$/i'); foreach ($regex as $file) { /* @var $file \SplFileInfo */ if (!$file->isFile()) { @@ -89,8 +90,22 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito private function getComposerInformation(\SplFileInfo $file) { $json = null; + $fileType = null; + $fileExtension = pathinfo($file->getPathname(), PATHINFO_EXTENSION); + if (in_array($fileExtension, array('gz', 'tar', 'tgz'), true)) { + $fileType = 'tar'; + } else if ($fileExtension === 'zip') { + $fileType = 'zip'; + } else { + throw new \RuntimeException('Files with "'.$fileExtension.'" extensions aren\'t supported. Only ZIP and TAR/TAR.GZ/TGZ archives are supported.'); + } + try { - $json = Zip::getComposerJson($file->getPathname()); + if ($fileType === 'tar') { + $json = Tar::getComposerJson($file->getPathname()); + } else { + $json = Zip::getComposerJson($file->getPathname()); + } } catch (\Exception $exception) { $this->io->write('Failed loading package '.$file->getPathname().': '.$exception->getMessage(), false, IOInterface::VERBOSE); } @@ -101,7 +116,7 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito $package = JsonFile::parseJson($json, $file->getPathname().'#composer.json'); $package['dist'] = array( - 'type' => 'zip', + 'type' => $fileType, 'url' => strtr($file->getPathname(), '\\', '/'), 'shasum' => sha1_file($file->getRealPath()), ); diff --git a/src/Composer/Util/Tar.php b/src/Composer/Util/Tar.php new file mode 100644 index 000000000..cae69c906 --- /dev/null +++ b/src/Composer/Util/Tar.php @@ -0,0 +1,68 @@ + + * 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 Wissem Riahi + */ +class Tar +{ + /** + * @param string $pathToArchive + * + * @return string|null + */ + public static function getComposerJson($pathToArchive) + { + $phar = new \PharData($pathToArchive); + + if (!$phar->valid()) { + return null; + } + + return self::extractComposerJsonFromFolder($phar); + } + + /** + * @param \PharData $phar + * + * @throws \RuntimeException + * + * @return string + */ + private static function extractComposerJsonFromFolder(\PharData $phar) + { + if (isset($phar['composer.json'])) { + return $phar['composer.json']->getContent(); + } + + $topLevelPaths = array(); + foreach ($phar as $folderFile) { + $name = $folderFile->getBasename(); + + if ($folderFile->isDir()) { + $topLevelPaths[$name] = true; + 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))); + } + } + } + + $composerJsonPath = key($topLevelPaths).'/composer.json'; + if ($topLevelPaths && isset($phar[$composerJsonPath])) { + return $phar[$composerJsonPath]->getContent(); + } + + throw new \RuntimeException('No composer.json found either at the top level or within the topmost directory'); + } +} diff --git a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php index 506a033c4..ad7a69009 100644 --- a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php +++ b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php @@ -36,6 +36,7 @@ class ArtifactRepositoryTest extends TestCase 'vendor1/package2-4.3.2', 'vendor3/package1-5.4.3', 'test/jsonInRoot-1.0.0', + 'test/jsonInRootTarFile-1.0.0', 'test/jsonInFirstLevel-1.0.0', //The files not-an-artifact.zip and jsonSecondLevel are not valid //artifacts and do not get detected. @@ -52,6 +53,13 @@ class ArtifactRepositoryTest extends TestCase sort($foundPackages); $this->assertSame($expectedPackages, $foundPackages); + + $tarPackage = array_filter($repo->getPackages(), function (BasePackage $package) { + return $package->getPrettyName() === 'test/jsonInRootTarFile'; + }); + $this->assertCount(1, $tarPackage); + $tarPackage = array_pop($tarPackage); + $this->assertSame('tar', $tarPackage->getDistType()); } public function testAbsoluteRepoUrlCreatesAbsoluteUrlPackages() diff --git a/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRooTarFile.tar.gz b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRooTarFile.tar.gz new file mode 100644 index 000000000..7d2938703 Binary files /dev/null and b/tests/Composer/Test/Repository/Fixtures/artifacts/jsonInRooTarFile.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/empty.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/empty.tar.gz new file mode 100644 index 000000000..805860f87 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/empty.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/folder.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/folder.tar.gz new file mode 100644 index 000000000..205472426 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/folder.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/multiple.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/multiple.tar.gz new file mode 100644 index 000000000..179a5bd97 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/multiple.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/nojson.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/nojson.tar.gz new file mode 100644 index 000000000..d6a64264d Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/nojson.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/root.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/root.tar.gz new file mode 100644 index 000000000..b2e272e90 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/root.tar.gz differ diff --git a/tests/Composer/Test/Util/Fixtures/Tar/subfolders.tar.gz b/tests/Composer/Test/Util/Fixtures/Tar/subfolders.tar.gz new file mode 100644 index 000000000..8fb347604 Binary files /dev/null and b/tests/Composer/Test/Util/Fixtures/Tar/subfolders.tar.gz differ diff --git a/tests/Composer/Test/Util/TarTest.php b/tests/Composer/Test/Util/TarTest.php new file mode 100644 index 000000000..b14d0c0f9 --- /dev/null +++ b/tests/Composer/Test/Util/TarTest.php @@ -0,0 +1,71 @@ + + * 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\Tar; +use Composer\Test\TestCase; + +/** + * @author Wissem Riahi + */ +class TarTest extends TestCase +{ + public function testReturnsNullifTheTarIsNotFound() + { + $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/invalid.zip'); + + $this->assertNull($result); + } + + public function testReturnsNullIfTheTarIsEmpty() + { + $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/empty.tar.gz'); + $this->assertNull($result); + } + + /** + * @expectedException \RuntimeException + */ + public function testThrowsExceptionIfTheTarHasNoComposerJson() + { + Tar::getComposerJson(__DIR__.'/Fixtures/Tar/nojson.tar.gz'); + } + + /** + * @expectedException \RuntimeException + */ + public function testThrowsExceptionIfTheComposerJsonIsInASubSubfolder() + { + Tar::getComposerJson(__DIR__.'/Fixtures/Tar/subfolders.tar.gz'); + } + + public function testReturnsComposerJsonInTarRoot() + { + $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/root.tar.gz'); + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } + + public function testReturnsComposerJsonInFirstFolder() + { + $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/folder.tar.gz'); + $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + } + + /** + * @expectedException \RuntimeException + */ + public function testMultipleTopLevelDirsIsInvalid() + { + Tar::getComposerJson(__DIR__.'/Fixtures/Tar/multiple.tar.gz'); + } +}