Browse Source

Merge remote-tracking branch 'palex-fpt/pear-channel-reader'

Jordi Boggiano 12 years ago
parent
commit
6fb667a752
40 changed files with 2214 additions and 466 deletions
  1. 37 1
      doc/05-repositories.md
  2. 0 52
      src/Composer/Downloader/PearDownloader.php
  3. 90 28
      src/Composer/Downloader/PearPackageExtractor.php
  4. 1 1
      src/Composer/Factory.php
  5. 32 8
      src/Composer/Installer/LibraryInstaller.php
  6. 95 0
      src/Composer/Installer/PearInstaller.php
  7. 81 0
      src/Composer/Repository/Pear/BaseChannelReader.php
  8. 67 0
      src/Composer/Repository/Pear/ChannelInfo.php
  9. 91 0
      src/Composer/Repository/Pear/ChannelReader.php
  10. 164 0
      src/Composer/Repository/Pear/ChannelRest10Reader.php
  11. 136 0
      src/Composer/Repository/Pear/ChannelRest11Reader.php
  12. 60 0
      src/Composer/Repository/Pear/DependencyConstraint.php
  13. 50 0
      src/Composer/Repository/Pear/DependencyInfo.php
  14. 314 0
      src/Composer/Repository/Pear/PackageDependencyParser.php
  15. 94 0
      src/Composer/Repository/Pear/PackageInfo.php
  16. 50 0
      src/Composer/Repository/Pear/ReleaseInfo.php
  17. 95 307
      src/Composer/Repository/PearRepository.php
  18. 11 1
      tests/Composer/Test/Downloader/Fixtures/Package_v2.1/package.xml
  19. 0 38
      tests/Composer/Test/Downloader/PearDownloaderTest.php
  20. 62 10
      tests/Composer/Test/Downloader/PearPackageExtractorTest.php
  21. 3 3
      tests/Composer/Test/Installer/LibraryInstallerTest.php
  22. 40 0
      tests/Composer/Test/Mock/RemoteFilesystemMock.php
  23. 151 0
      tests/Composer/Test/Repository/Pear/ChannelReaderTest.php
  24. 41 0
      tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php
  25. 37 0
      tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php
  26. 167 0
      tests/Composer/Test/Repository/Pear/Fixtures/DependencyParserTestData.json
  27. 9 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_allreleases.xml
  28. 1 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_deps.1.2.1.txt
  29. 14 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_info.xml
  30. 9 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_allreleases.xml
  31. 1 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_deps.1.4.0.txt
  32. 12 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_info.xml
  33. 6 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/packages.xml
  34. 5 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.1/categories.xml
  35. 93 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.1/packagesinfo.xml
  36. 12 0
      tests/Composer/Test/Repository/Pear/Fixtures/channel.1.0.xml
  37. 12 0
      tests/Composer/Test/Repository/Pear/Fixtures/channel.1.1.xml
  38. 58 0
      tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php
  39. 11 17
      tests/Composer/Test/Repository/PearRepositoryTest.php
  40. 2 0
      tests/bootstrap.php

+ 37 - 1
doc/05-repositories.md

@@ -202,7 +202,7 @@ should you need to specify one for whatever reason, you can use `git`, `svn` or
 
 It is possible to install packages from any PEAR channel by using the `pear`
 repository. Composer will prefix all package names with `pear-{channelName}/` to
-avoid conflicts.
+avoid conflicts. All packages are also aliased with prefix `pear-{channelAlias}/`
 
 Example using `pear2.php.net`:
 
@@ -214,6 +214,7 @@ Example using `pear2.php.net`:
             }
         ],
         "require": {
+            "pear-pear2.php.net/PEAR2_Text_Markdown": "*",
             "pear-pear2/PEAR2_HTTP_Request": "*"
         }
     }
@@ -224,6 +225,41 @@ In this case the short name of the channel is `pear2`, so the
 > **Note:** The `pear` repository requires doing quite a few requests per
 > package, so this may considerably slow down the installation process.
 
+#### Custom channel alias
+It is possible to alias all pear channel packages with custom name.
+
+Example:
+You own private pear repository and going to use composer abilities to bring dependencies from vcs or transit to composer repository scheme.
+Your repository list of packages:
+ * BasePackage, requires nothing
+ * IntermediatePackage, depends on BasePackage
+ * TopLevelPackage1 and TopLevelPackage2 both dependth on IntermediatePackage.
+
+For composer it looks like:
+ * "pear-pear.foobar.repo/IntermediatePackage" depends on "pear-pear.foobar.repo/BasePackage",
+ * "pear-pear.foobar.repo/TopLevelPackage1" depends on "pear-pear.foobar.repo/IntermediatePackage",
+ * "pear-pear.foobar.repo/TopLevelPackage2" depends on "pear-pear.foobar.repo/IntermediatePackage"
+
+When you update one of your packages to composer naming scheme or made it available through vcs, your older dependencies would not see new version, cause it would be named like "foobar/IntermediatePackage". Specifying 'vendor-alias' for pear repository, you will get all its packages aliased with composer-like names. Following example would take BasePackage, TopLevelPackage1 and TopLevelPackage2 packages from pear repository and IntermediatePackage from github repository:
+
+    {
+        "repositories": [
+            {
+                "type": "git",
+                "https://github.com/foobar/intermediate.git"
+            },
+            {
+                "type": "pear",
+                "url": "http://pear.foobar.repo",
+                "vendor-alias": "foobar"
+            }
+        ],
+        "require": {
+            "foobar/TopLevelPackage1": "*",
+            "foobar/TopLevelPackage2": "*"
+        }
+    }
+
 ### Package
 
 If you want to use a project that does not support composer through any of the

+ 0 - 52
src/Composer/Downloader/PearDownloader.php

@@ -1,52 +0,0 @@
-<?php
-
-/*
- * This file is part of Composer.
- *
- * (c) Nils Adermann <naderman@naderman.de>
- *     Jordi Boggiano <j.boggiano@seld.be>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Composer\Downloader;
-
-use Composer\Package\PackageInterface;
-
-/**
- * Downloader for pear packages
- *
- * @author Jordi Boggiano <j.boggiano@seld.be>
- * @author Kirill chEbba Chebunin <iam@chebba.org>
- */
-class PearDownloader extends FileDownloader
-{
-    /**
-     * {@inheritDoc}
-     */
-    public function download(PackageInterface $package, $path)
-    {
-        parent::download($package, $path);
-
-        $fileName = $this->getFileName($package, $path);
-        if ($this->io->isVerbose()) {
-            $this->io->write('    Installing PEAR package');
-        }
-        try {
-            $pearExtractor = new PearPackageExtractor($fileName);
-            $pearExtractor->extractTo($path);
-
-            if ($this->io->isVerbose()) {
-                $this->io->write('    Cleaning up');
-            }
-            unlink($fileName);
-        } catch (\Exception $e) {
-            // clean up
-            $this->filesystem->removeDirectory($path);
-            throw $e;
-        }
-
-        $this->io->write('');
-    }
-}

+ 90 - 28
src/Composer/Downloader/PearPackageExtractor.php

@@ -35,22 +35,22 @@ class PearPackageExtractor
             throw new \UnexpectedValueException('PEAR package file is not found at '.$file);
         }
 
+        $this->filesystem = new Filesystem();
         $this->file = $file;
     }
 
     /**
      * Installs PEAR source files according to package.xml definitions and removes extracted files
      *
-     * @param $file   string path to downloaded PEAR archive file
-     * @param $target string target install location. all source installation would be performed relative to target path.
-     * @param $role   string type of files to install. default role for PEAR source files are 'php'.
-     *
+     * @param  string                    $target target install location. all source installation would be performed relative to target path.
+     * @param  array                     $roles  types of files to install. default role for PEAR source files are 'php'.
+     * @param  array                     $vars   used for replacement tasks
      * @throws \RuntimeException
+     * @throws \UnexpectedValueException
+     *
      */
-    public function extractTo($target, $role = 'php')
+    public function extractTo($target, array $roles = array('php' => '/', 'script' => '/bin'), $vars = array())
     {
-        $this->filesystem = new Filesystem();
-
         $extractionPath = $target.'/tarball';
 
         try {
@@ -61,8 +61,8 @@ class PearPackageExtractor
                 throw new \RuntimeException('Invalid PEAR package. It must contain package.xml file.');
             }
 
-            $fileCopyActions = $this->buildCopyActions($extractionPath, $role);
-            $this->copyFiles($fileCopyActions, $extractionPath, $target);
+            $fileCopyActions = $this->buildCopyActions($extractionPath, $roles, $vars);
+            $this->copyFiles($fileCopyActions, $extractionPath, $target, $roles, $vars);
             $this->filesystem->removeDirectory($extractionPath);
         } catch (\Exception $exception) {
             throw new \UnexpectedValueException(sprintf('Failed to extract PEAR package %s to %s. Reason: %s', $this->file, $target, $exception->getMessage()), 0, $exception);
@@ -72,20 +72,24 @@ class PearPackageExtractor
     /**
      * Perform copy actions on files
      *
-     * @param $files array array('from', 'to') with relative paths
+     * @param array $files array of copy actions ('from', 'to') with relative paths
      * @param $source string path to source dir.
      * @param $target string path to destination dir
+     * @param array $roles array [role => roleRoot] relative root for files having that role
+     * @param array $vars  list of values can be used for replacement tasks
      */
-    private function copyFiles($files, $source, $target)
+    private function copyFiles($files, $source, $target, $roles, $vars)
     {
         foreach ($files as $file) {
             $from = $this->combine($source, $file['from']);
-            $to = $this->combine($target, $file['to']);
-            $this->copyFile($from, $to);
+            $to = $this->combine($target, $roles[$file['role']]);
+            $to = $this->combine($to, $file['to']);
+            $tasks = $file['tasks'];
+            $this->copyFile($from, $to, $tasks, $vars);
         }
     }
 
-    private function copyFile($from, $to)
+    private function copyFile($from, $to, $tasks, $vars)
     {
         if (!is_file($from)) {
             throw new \RuntimeException('Invalid PEAR package. package.xml defines file that is not located inside tarball.');
@@ -93,7 +97,24 @@ class PearPackageExtractor
 
         $this->filesystem->ensureDirectoryExists(dirname($to));
 
-        if (!copy($from, $to)) {
+        if (0 == count($tasks)) {
+            $copied = copy($from, $to);
+        } else {
+            $content = file_get_contents($from);
+            $replacements = array();
+            foreach ($tasks as $task) {
+                $pattern = $task['from'];
+                $varName = $task['to'];
+                if (isset($vars[$varName])) {
+                    $replacements[$pattern] = $vars[$varName];
+                }
+            }
+            $content = strtr($content, $replacements);
+
+            $copied = file_put_contents($to, $content);
+        }
+
+        if (false === $copied) {
             throw new \RuntimeException(sprintf('Failed to copy %s to %s', $from, $to));
         }
     }
@@ -107,7 +128,7 @@ class PearPackageExtractor
      *  path, and target is destination of file (also relative to $source path)
      * @throws \RuntimeException
      */
-    private function buildCopyActions($source, $role)
+    private function buildCopyActions($source, array $roles, $vars)
     {
         /** @var $package \SimpleXmlElement */
         $package = simplexml_load_file($this->combine($source, 'package.xml'));
@@ -120,13 +141,18 @@ class PearPackageExtractor
             $packageName = (string) $package->name;
             $packageVersion = (string) $package->release->version;
             $sourceDir = $packageName . '-' . $packageVersion;
-            $result = $this->buildSourceList10($children, $role, $sourceDir);
+            $result = $this->buildSourceList10($children, $roles, $sourceDir);
         } elseif ('2.0' == $packageSchemaVersion || '2.1' == $packageSchemaVersion) {
             $children = $package->contents->children();
             $packageName = (string) $package->name;
             $packageVersion = (string) $package->version->release;
             $sourceDir = $packageName . '-' . $packageVersion;
-            $result = $this->buildSourceList20($children, $role, $sourceDir);
+            $result = $this->buildSourceList20($children, $roles, $sourceDir);
+
+            $namespaces = $package->getNamespaces();
+            $package->registerXPathNamespace('ns', $namespaces['']);
+            $releaseNodes = $package->xpath('ns:phprelease');
+            $this->applyRelease($result, $releaseNodes, $vars);
         } else {
             throw new \RuntimeException('Unsupported schema version of package definition file.');
         }
@@ -134,7 +160,35 @@ class PearPackageExtractor
         return $result;
     }
 
-    private function buildSourceList10($children, $targetRole, $source = '', $target = '', $role = null)
+    private function applyRelease(&$actions, $releaseNodes, $vars)
+    {
+        foreach ($releaseNodes as $releaseNode) {
+            $requiredOs = $releaseNode->installconditions && $releaseNode->installconditions->os && $releaseNode->installconditions->os->name ? (string) $releaseNode->installconditions->os->name : '';
+            if ($requiredOs && $vars['os'] != $requiredOs) {
+                continue;
+            }
+
+            if ($releaseNode->filelist) {
+                foreach ($releaseNode->filelist->children() as $action) {
+                    if ('install' == $action->getName()) {
+                        $name = (string) $action['name'];
+                        $as = (string) $action['as'];
+                        if (isset($actions[$name])) {
+                            $actions[$name]['to'] = $as;
+                        }
+                    } elseif ('ignore' == $action->getName()) {
+                        $name = (string) $action['name'];
+                        unset($actions[$name]);
+                    } else {
+                        // unknown action
+                    }
+                }
+            }
+            break;
+        }
+    }
+
+    private function buildSourceList10($children, $targetRoles, $source = '', $target = '', $role = null)
     {
         $result = array();
 
@@ -145,14 +199,15 @@ class PearPackageExtractor
                 $dirSource = $this->combine($source, (string) $child['name']);
                 $dirTarget = $child['baseinstalldir'] ? : $target;
                 $dirRole = $child['role'] ? : $role;
-                $dirFiles = $this->buildSourceList10($child->children(), $targetRole, $dirSource, $dirTarget, $dirRole);
+                $dirFiles = $this->buildSourceList10($child->children(), $targetRoles, $dirSource, $dirTarget, $dirRole);
                 $result = array_merge($result, $dirFiles);
             } elseif ($child->getName() == 'file') {
-                if (($child['role'] ? : $role) == $targetRole) {
+                $fileRole = (string) $child['role'] ? : $role;
+                if (isset($targetRoles[$fileRole])) {
                     $fileName = (string) ($child['name'] ? : $child[0]); // $child[0] means text content
                     $fileSource = $this->combine($source, $fileName);
                     $fileTarget = $this->combine((string) $child['baseinstalldir'] ? : $target, $fileName);
-                    $result[] = array('from' => $fileSource, 'to' => $fileTarget);
+                    $result[(string) $child['name']] = array('from' => $fileSource, 'to' => $fileTarget, 'role' => $fileRole, 'tasks' => array());
                 }
             }
         }
@@ -160,24 +215,31 @@ class PearPackageExtractor
         return $result;
     }
 
-    private function buildSourceList20($children, $targetRole, $source = '', $target = '', $role = null)
+    private function buildSourceList20($children, $targetRoles, $source = '', $target = '', $role = null)
     {
         $result = array();
 
         // enumerating files
         foreach ($children as $child) {
             /** @var $child \SimpleXMLElement */
-            if ($child->getName() == 'dir') {
+            if ('dir' == $child->getName()) {
                 $dirSource = $this->combine($source, $child['name']);
                 $dirTarget = $child['baseinstalldir'] ? : $target;
                 $dirRole = $child['role'] ? : $role;
-                $dirFiles = $this->buildSourceList20($child->children(), $targetRole, $dirSource, $dirTarget, $dirRole);
+                $dirFiles = $this->buildSourceList20($child->children(), $targetRoles, $dirSource, $dirTarget, $dirRole);
                 $result = array_merge($result, $dirFiles);
-            } elseif ($child->getName() == 'file') {
-                if (($child['role'] ? : $role) == $targetRole) {
+            } elseif ('file' == $child->getName()) {
+                $fileRole = (string) $child['role'] ? : $role;
+                if (isset($targetRoles[$fileRole])) {
                     $fileSource = $this->combine($source, (string) $child['name']);
                     $fileTarget = $this->combine((string) ($child['baseinstalldir'] ? : $target), (string) $child['name']);
-                    $result[] = array('from' => $fileSource, 'to' => $fileTarget);
+                    $fileTasks = array();
+                    foreach ($child->children('http://pear.php.net/dtd/tasks-1.0') as $taskNode) {
+                        if ('replace' == $taskNode->getName()) {
+                            $fileTasks[] = array('from' => (string) $taskNode->attributes()->from, 'to' => (string) $taskNode->attributes()->to);
+                        }
+                    }
+                    $result[(string) $child['name']] = array('from' => $fileSource, 'to' => $fileTarget, 'role' => $fileRole, 'tasks' => $fileTasks);
                 }
             }
         }

+ 1 - 1
src/Composer/Factory.php

@@ -225,7 +225,6 @@ class Factory
         $dm->setDownloader('git', new Downloader\GitDownloader($io));
         $dm->setDownloader('svn', new Downloader\SvnDownloader($io));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io));
-        $dm->setDownloader('pear', new Downloader\PearDownloader($io));
         $dm->setDownloader('zip', new Downloader\ZipDownloader($io));
         $dm->setDownloader('tar', new Downloader\TarDownloader($io));
         $dm->setDownloader('phar', new Downloader\PharDownloader($io));
@@ -251,6 +250,7 @@ class Factory
     protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io)
     {
         $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null));
+        $im->addInstaller(new Installer\PearInstaller($io, $composer, 'pear-library'));
         $im->addInstaller(new Installer\InstallerInstaller($io, $composer));
         $im->addInstaller(new Installer\MetapackageInstaller($io));
     }

+ 32 - 8
src/Composer/Installer/LibraryInstaller.php

@@ -82,7 +82,7 @@ class LibraryInstaller implements InstallerInterface
             $this->removeBinaries($package);
         }
 
-        $this->downloadManager->download($package, $downloadPath);
+        $this->installCode($package);
         $this->installBinaries($package);
         if (!$repo->hasPackage($package)) {
             $repo->addPackage(clone $package);
@@ -99,10 +99,9 @@ class LibraryInstaller implements InstallerInterface
         }
 
         $this->initializeVendorDir();
-        $downloadPath = $this->getInstallPath($initial);
 
         $this->removeBinaries($initial);
-        $this->downloadManager->update($initial, $target, $downloadPath);
+        $this->updateCode($initial, $target);
         $this->installBinaries($target);
         $repo->removePackage($initial);
         if (!$repo->hasPackage($target)) {
@@ -123,7 +122,7 @@ class LibraryInstaller implements InstallerInterface
 
         $downloadPath = $this->getInstallPath($package);
 
-        $this->downloadManager->remove($package, $downloadPath);
+        $this->removeCode($package);
         $this->removeBinaries($package);
         $repo->removePackage($package);
 
@@ -146,12 +145,36 @@ class LibraryInstaller implements InstallerInterface
         return ($this->vendorDir ? $this->vendorDir.'/' : '') . $package->getPrettyName() . ($targetDir ? '/'.$targetDir : '');
     }
 
+    protected function installCode(PackageInterface $package)
+    {
+        $downloadPath = $this->getInstallPath($package);
+        $this->downloadManager->download($package, $downloadPath);
+    }
+
+    protected function updateCode(PackageInterface $initial, PackageInterface $target)
+    {
+        $downloadPath = $this->getInstallPath($initial);
+        $this->downloadManager->update($initial, $target, $downloadPath);
+    }
+
+    protected function removeCode(PackageInterface $package)
+    {
+        $downloadPath = $this->getInstallPath($package);
+        $this->downloadManager->remove($package, $downloadPath);
+    }
+
+    protected function getBinaries(PackageInterface $package)
+    {
+        return $package->getBinaries();
+    }
+
     protected function installBinaries(PackageInterface $package)
     {
-        if (!$package->getBinaries()) {
+        $binaries = $this->getBinaries($package);
+        if (!$binaries) {
             return;
         }
-        foreach ($package->getBinaries() as $bin) {
+        foreach ($binaries as $bin) {
             $this->initializeBinDir();
             $link = $this->binDir.'/'.basename($bin);
             if (file_exists($link)) {
@@ -193,10 +216,11 @@ class LibraryInstaller implements InstallerInterface
 
     protected function removeBinaries(PackageInterface $package)
     {
-        if (!$package->getBinaries()) {
+        $binaries = $this->getBinaries($package);
+        if (!$binaries) {
             return;
         }
-        foreach ($package->getBinaries() as $bin) {
+        foreach ($binaries as $bin) {
             $link = $this->binDir.'/'.basename($bin);
             if (!file_exists($link)) {
                 continue;

+ 95 - 0
src/Composer/Installer/PearInstaller.php

@@ -0,0 +1,95 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Installer;
+
+use Composer\IO\IOInterface;
+use Composer\Composer;
+use Composer\Downloader\PearPackageExtractor;
+use Composer\Downloader\DownloadManager;
+use Composer\Repository\InstalledRepositoryInterface;
+use Composer\Package\PackageInterface;
+use Composer\Util\Filesystem;
+
+/**
+ * Package installation manager.
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+class PearInstaller extends LibraryInstaller
+{
+    private $filesystem;
+
+    /**
+     * Initializes library installer.
+     *
+     * @param string          $vendorDir relative path for packages home
+     * @param string          $binDir    relative path for binaries
+     * @param DownloadManager $dm        download manager
+     * @param IOInterface     $io        io instance
+     * @param string          $type      package type that this installer handles
+     */
+    public function __construct(IOInterface $io, Composer $composer, $type = 'pear-library')
+    {
+        $this->filesystem = new Filesystem();
+        parent::__construct($io, $composer, $type);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
+    {
+        $this->uninstall($repo, $initial);
+        $this->install($repo, $target);
+    }
+
+    protected function installCode(PackageInterface $package)
+    {
+        parent::installCode($package);
+
+        $isWindows = defined('PHP_WINDOWS_VERSION_BUILD') ? true : false;
+
+        $vars = array(
+            'os' => $isWindows ? 'windows' : 'linux',
+            'php_bin' => ($isWindows ? getenv('PHPRC') .'php.exe' : trim(`which php`)),
+            'pear_php' => $this->getInstallPath($package),
+            'bin_dir' => $this->getInstallPath($package) . '/bin',
+            'php_dir' => $this->getInstallPath($package),
+            'data_dir' => '@DATA_DIR@',
+            'version' => $package->getPrettyVersion(),
+        );
+
+        $packageArchive = $this->getInstallPath($package).'/'.pathinfo($package->getDistUrl(), PATHINFO_BASENAME);
+        $pearExtractor = new PearPackageExtractor($packageArchive);
+        $pearExtractor->extractTo($this->getInstallPath($package), array('php' => '/', 'script' => '/bin'), $vars);
+
+        if ($this->io->isVerbose()) {
+            $this->io->write('    Cleaning up');
+        }
+        unlink($packageArchive);
+    }
+
+    protected function getBinaries(PackageInterface $package)
+    {
+        $binariesPath = $this->getInstallPath($package) . '/bin/';
+        $binaries = array();
+        if (file_exists($binariesPath)) {
+            foreach (new \FilesystemIterator($binariesPath, \FilesystemIterator::KEY_AS_FILENAME) as $fileName => $value) {
+                $binaries[] = 'bin/'.$fileName;
+            }
+        }
+
+        return $binaries;
+    }
+}

+ 81 - 0
src/Composer/Repository/Pear/BaseChannelReader.php

@@ -0,0 +1,81 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+use Composer\Util\RemoteFilesystem;
+
+/**
+ * Base PEAR Channel reader.
+ *
+ * Provides xml namespaces and red
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+abstract class BaseChannelReader
+{
+    /**
+     * PEAR REST Interface namespaces
+     */
+    const CHANNEL_NS = 'http://pear.php.net/channel-1.0';
+    const ALL_CATEGORIES_NS = 'http://pear.php.net/dtd/rest.allcategories';
+    const CATEGORY_PACKAGES_INFO_NS = 'http://pear.php.net/dtd/rest.categorypackageinfo';
+    const ALL_PACKAGES_NS = 'http://pear.php.net/dtd/rest.allpackages';
+    const ALL_RELEASES_NS = 'http://pear.php.net/dtd/rest.allreleases';
+    const PACKAGE_INFO_NS = 'http://pear.php.net/dtd/rest.package';
+
+    /** @var RemoteFilesystem */
+    private $rfs;
+
+    protected function __construct(RemoteFilesystem $rfs)
+    {
+        $this->rfs = $rfs;
+    }
+
+    /**
+     * Read content from remote filesystem.
+     *
+     * @param $origin string server
+     * @param $path   string relative path to content
+     * @return \SimpleXMLElement
+     */
+    protected function requestContent($origin, $path)
+    {
+        $url = rtrim($origin, '/') . '/' . ltrim($path, '/');
+        $content = $this->rfs->getContents($origin, $url, false);
+        if (!$content) {
+            throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.');
+        }
+
+        return $content;
+    }
+
+    /**
+     * Read xml content from remote filesystem
+     *
+     * @param $origin string server
+     * @param $path   string relative path to content
+     * @return \SimpleXMLElement
+     */
+    protected function requestXml($origin, $path)
+    {
+        // http://components.ez.no/p/packages.xml is malformed. to read it we must ignore parsing errors.
+        $xml = simplexml_load_string($this->requestContent($origin, $path), "SimpleXMLElement", LIBXML_NOERROR);
+
+        if (false == $xml) {
+            $url = rtrim($origin, '/') . '/' . ltrim($path, '/');
+            throw new \UnexpectedValueException(sprintf('The PEAR channel at ' . $origin . ' is broken. (Invalid XML at file `%s`)', $path));
+        }
+
+        return $xml;
+    }
+}

+ 67 - 0
src/Composer/Repository/Pear/ChannelInfo.php

@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+/**
+ * PEAR channel info
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class ChannelInfo
+{
+    private $name;
+    private $alias;
+    private $packages;
+
+    /**
+     * @param string        $name
+     * @param string        $alias
+     * @param PackageInfo[] $packages
+     */
+    public function __construct($name, $alias, array $packages)
+    {
+        $this->name = $name;
+        $this->alias = $alias;
+        $this->packages = $packages;
+    }
+
+    /**
+     * Name of the channel
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Alias of the channel
+     *
+     * @return string
+     */
+    public function getAlias()
+    {
+        return $this->alias;
+    }
+
+    /**
+     * List of channel packages
+     *
+     * @return PackageInfo[]
+     */
+    public function getPackages()
+    {
+        return $this->packages;
+    }
+}

+ 91 - 0
src/Composer/Repository/Pear/ChannelReader.php

@@ -0,0 +1,91 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+use Composer\Util\RemoteFilesystem;
+
+/**
+ * PEAR Channel package reader.
+ *
+ * Reads channel packages info from and builds MemoryPackage's
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class ChannelReader extends BaseChannelReader
+{
+    /** @var array of ('xpath test' => 'rest implementation') */
+    private $readerMap;
+
+    public function __construct(RemoteFilesystem $rfs)
+    {
+        parent::__construct($rfs);
+
+        $rest10reader = new ChannelRest10Reader($rfs);
+        $rest11reader = new ChannelRest11Reader($rfs);
+
+        $this->readerMap = array(
+            'REST1.3' => $rest11reader,
+            'REST1.2' => $rest11reader,
+            'REST1.1' => $rest11reader,
+            'REST1.0' => $rest10reader,
+        );
+    }
+
+    /**
+     * Reads PEAR channel through REST interface and builds list of packages
+     *
+     * @param $url string PEAR Channel url
+     * @return ChannelInfo
+     */
+    public function read($url)
+    {
+        $xml = $this->requestXml($url, "/channel.xml");
+
+        $channelName = (string) $xml->name;
+        $channelSummary = (string) $xml->summary;
+        $channelAlias = (string) $xml->suggestedalias;
+
+        $supportedVersions = array_keys($this->readerMap);
+        $selectedRestVersion = $this->selectRestVersion($xml, $supportedVersions);
+        if (!$selectedRestVersion) {
+            throw new \UnexpectedValueException(sprintf('PEAR repository %s does not supports any of %s protocols.', $url, implode(', ', $supportedVersions)));
+        }
+
+        $reader = $this->readerMap[$selectedRestVersion['version']];
+        $packageDefinitions = $reader->read($selectedRestVersion['baseUrl']);
+
+        return new ChannelInfo($channelName, $channelAlias, $packageDefinitions);
+    }
+
+    /**
+     * Reads channel supported REST interfaces and selects one of them
+     *
+     * @param $channelXml \SimpleXMLElement
+     * @param $supportedVersions string[] supported PEAR REST protocols
+     * @return array|null hash with selected version and baseUrl
+     */
+    private function selectRestVersion($channelXml, $supportedVersions)
+    {
+        $channelXml->registerXPathNamespace('ns', self::CHANNEL_NS);
+
+        foreach ($supportedVersions as $version) {
+            $xpathTest = "ns:servers/ns:primary/ns:rest/ns:baseurl[@type='{$version}']";
+            $testResult = $channelXml->xpath($xpathTest);
+            if (count($testResult) > 0) {
+                return array('version' => $version, 'baseUrl' => (string) $testResult[0]);
+            }
+        }
+
+        return null;
+    }
+}

+ 164 - 0
src/Composer/Repository/Pear/ChannelRest10Reader.php

@@ -0,0 +1,164 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+use Composer\Downloader\TransportException;
+
+/**
+ * Read PEAR packages using REST 1.0 interface
+ *
+ * At version 1.0 package descriptions read from:
+ *  {baseUrl}/p/packages.xml
+ *  {baseUrl}/p/{package}/info.xml
+ *  {baseUrl}/p/{package}/allreleases.xml
+ *  {baseUrl}/p/{package}/deps.{version}.txt
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class ChannelRest10Reader extends BaseChannelReader
+{
+    private $dependencyReader;
+
+    public function __construct($rfs)
+    {
+        parent::__construct($rfs);
+
+        $this->dependencyReader = new PackageDependencyParser();
+    }
+
+    /**
+     * Reads package descriptions using PEAR Rest 1.0 interface
+     *
+     * @param $baseUrl  string base Url interface
+     *
+     * @return PackageInfo[]
+     */
+    public function read($baseUrl)
+    {
+        return $this->readPackages($baseUrl);
+    }
+
+    /**
+     * Read list of packages from
+     *  {baseUrl}/p/packages.xml
+     *
+     * @param $baseUrl string
+     * @return PackageInfo[]
+     */
+    private function readPackages($baseUrl)
+    {
+        $result = array();
+
+        $xmlPath = '/p/packages.xml';
+        $xml = $this->requestXml($baseUrl, $xmlPath);
+        $xml->registerXPathNamespace('ns', self::ALL_PACKAGES_NS);
+        foreach ($xml->xpath('ns:p') as $node) {
+            $packageName = (string) $node;
+            $packageInfo = $this->readPackage($baseUrl, $packageName);
+            $result[] = $packageInfo;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Read package info from
+     *  {baseUrl}/p/{package}/info.xml
+     *
+     * @param $baseUrl      string
+     * @param $packageName  string
+     * @return PackageInfo
+     */
+    private function readPackage($baseUrl, $packageName)
+    {
+        $xmlPath = '/p/' . strtolower($packageName) . '/info.xml';
+        $xml = $this->requestXml($baseUrl, $xmlPath);
+        $xml->registerXPathNamespace('ns', self::PACKAGE_INFO_NS);
+
+        $channelName = (string) $xml->c;
+        $packageName = (string) $xml->n;
+        $license = (string) $xml->l;
+        $shortDescription = (string) $xml->s;
+        $description = (string) $xml->d;
+
+        return new PackageInfo(
+            $channelName,
+            $packageName,
+            $license,
+            $shortDescription,
+            $description,
+            $this->readPackageReleases($baseUrl, $packageName)
+        );
+    }
+
+    /**
+     * Read package releases from
+     *  {baseUrl}/p/{package}/allreleases.xml
+     *
+     * @param $baseUrl      string
+     * @param $packageName  string
+     * @return ReleaseInfo[] hash array with keys as version numbers
+     */
+    private function readPackageReleases($baseUrl, $packageName)
+    {
+        $result = array();
+
+        try {
+            $xmlPath = '/r/' . strtolower($packageName) . '/allreleases.xml';
+            $xml = $this->requestXml($baseUrl, $xmlPath);
+            $xml->registerXPathNamespace('ns', self::ALL_RELEASES_NS);
+            foreach ($xml->xpath('ns:r') as $node) {
+                $releaseVersion = (string) $node->v;
+                $releaseStability = (string) $node->s;
+
+                try {
+                    $result[$releaseVersion] = new ReleaseInfo(
+                        $releaseStability,
+                        $this->readPackageReleaseDependencies($baseUrl, $packageName, $releaseVersion)
+                    );
+                } catch (TransportException $exception) {
+                    if ($exception->getCode() != 404) {
+                        throw $exception;
+                    }
+                }
+            }
+        } catch (TransportException $exception) {
+            if ($exception->getCode() != 404) {
+                throw $exception;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Read package dependencies from
+     *  {baseUrl}/p/{package}/deps.{version}.txt
+     *
+     * @param $baseUrl      string
+     * @param $packageName  string
+     * @param $version      string
+     * @return DependencyInfo[]
+     */
+    private function readPackageReleaseDependencies($baseUrl, $packageName, $version)
+    {
+        $dependencyReader = new PackageDependencyParser();
+
+        $depthPath = '/r/' . strtolower($packageName) . '/deps.' . $version . '.txt';
+        $content = $this->requestContent($baseUrl, $depthPath);
+        $dependencyArray = unserialize($content);
+        $result = $dependencyReader->buildDependencyInfo($dependencyArray);
+
+        return $result;
+    }
+}

+ 136 - 0
src/Composer/Repository/Pear/ChannelRest11Reader.php

@@ -0,0 +1,136 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+/**
+ * Read PEAR packages using REST 1.1 interface
+ *
+ * At version 1.1 package descriptions read from:
+ *  {baseUrl}/c/categories.xml
+ *  {baseUrl}/c/{category}/packagesinfo.xml
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class ChannelRest11Reader extends BaseChannelReader
+{
+    private $dependencyReader;
+
+    public function __construct($rfs)
+    {
+        parent::__construct($rfs);
+
+        $this->dependencyReader = new PackageDependencyParser();
+    }
+
+    /**
+     * Reads package descriptions using PEAR Rest 1.1 interface
+     *
+     * @param $baseUrl  string base Url interface
+     *
+     * @return PackageInfo[]
+     */
+    public function read($baseUrl)
+    {
+        return $this->readChannelPackages($baseUrl);
+    }
+
+    /**
+     * Read list of channel categories from
+     *  {baseUrl}/c/categories.xml
+     *
+     * @param $baseUrl string
+     * @return PackageInfo[]
+     */
+    private function readChannelPackages($baseUrl)
+    {
+        $result = array();
+
+        $xml = $this->requestXml($baseUrl, "/c/categories.xml");
+        $xml->registerXPathNamespace('ns', self::ALL_CATEGORIES_NS);
+        foreach ($xml->xpath('ns:c') as $node) {
+            $categoryName = (string) $node;
+            $categoryPackages = $this->readCategoryPackages($baseUrl, $categoryName);
+            $result = array_merge($result, $categoryPackages);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Read packages from
+     *  {baseUrl}/c/{category}/packagesinfo.xml
+     *
+     * @param $baseUrl      string
+     * @param $categoryName string
+     * @return PackageInfo[]
+     */
+    private function readCategoryPackages($baseUrl, $categoryName)
+    {
+        $result = array();
+
+        $categoryPath = '/c/'.urlencode($categoryName).'/packagesinfo.xml';
+        $xml = $this->requestXml($baseUrl, $categoryPath);
+        $xml->registerXPathNamespace('ns', self::CATEGORY_PACKAGES_INFO_NS);
+        foreach ($xml->xpath('ns:pi') as $node) {
+            $packageInfo = $this->parsePackage($node);
+            $result[] = $packageInfo;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Parses package node.
+     *
+     * @param $packageInfo  \SimpleXMLElement   xml element describing package
+     * @return PackageInfo
+     */
+    private function parsePackage($packageInfo)
+    {
+        $packageInfo->registerXPathNamespace('ns', self::CATEGORY_PACKAGES_INFO_NS);
+        $channelName = (string) $packageInfo->p->c;
+        $packageName = (string) $packageInfo->p->n;
+        $license = (string) $packageInfo->p->l;
+        $shortDescription = (string) $packageInfo->p->s;
+        $description = (string) $packageInfo->p->d;
+
+        $dependencies = array();
+        foreach ($packageInfo->xpath('ns:deps') as $node) {
+            $dependencyVersion = (string) $node->v;
+            $dependencyArray = unserialize((string) $node->d);
+
+            $dependencyInfo = $this->dependencyReader->buildDependencyInfo($dependencyArray);
+
+            $dependencies[$dependencyVersion] = $dependencyInfo;
+        }
+
+        $releases = array();
+        foreach ($packageInfo->xpath('ns:a/ns:r') as $node) {
+            $releaseVersion = (string) $node->v;
+            $releaseStability = (string) $node->s;
+            $releases[$releaseVersion] = new ReleaseInfo(
+                $releaseStability,
+                isset($dependencies[$releaseVersion]) ? $dependencies[$releaseVersion] : new DependencyInfo(array(), array())
+            );
+        }
+
+        return new PackageInfo(
+            $channelName,
+            $packageName,
+            $license,
+            $shortDescription,
+            $description,
+            $releases
+        );
+    }
+}

+ 60 - 0
src/Composer/Repository/Pear/DependencyConstraint.php

@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+/**
+ * PEAR package release dependency info
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class DependencyConstraint
+{
+    private $type;
+    private $constraint;
+    private $channelName;
+    private $packageName;
+
+    /**
+     * @param string $type
+     * @param string $constraint
+     * @param string $channelName
+     * @param string $packageName
+     */
+    public function __construct($type, $constraint, $channelName, $packageName)
+    {
+        $this->type = $type;
+        $this->constraint = $constraint;
+        $this->channelName = $channelName;
+        $this->packageName = $packageName;
+    }
+
+    public function getChannelName()
+    {
+        return $this->channelName;
+    }
+
+    public function getConstraint()
+    {
+        return $this->constraint;
+    }
+
+    public function getPackageName()
+    {
+        return $this->packageName;
+    }
+
+    public function getType()
+    {
+        return $this->type;
+    }
+}

+ 50 - 0
src/Composer/Repository/Pear/DependencyInfo.php

@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+/**
+ * PEAR package release dependency info
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class DependencyInfo
+{
+    private $requires;
+    private $optionals;
+
+    /**
+     * @param DependencyConstraint[] $requires  list of requires/conflicts/replaces
+     * @param array                  $optionals [groupName => DependencyConstraint[]] list of optional groups
+     */
+    public function __construct($requires, $optionals)
+    {
+        $this->requires = $requires;
+        $this->optionals = $optionals;
+    }
+
+    /**
+     * @return DependencyConstraint[] list of requires/conflicts/replaces
+     */
+    public function getRequires()
+    {
+        return $this->requires;
+    }
+
+    /**
+     * @return array [groupName => DependencyConstraint[]] list of optional groups
+     */
+    public function getOptionals()
+    {
+        return $this->optionals;
+    }
+}

+ 314 - 0
src/Composer/Repository/Pear/PackageDependencyParser.php

@@ -0,0 +1,314 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+/**
+ * Read PEAR packages using REST 1.0 interface
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class PackageDependencyParser
+{
+    /**
+     * Builds dependency information. It detects used package.xml format.
+     *
+     * @param $depArray array
+     * @return DependencyInfo
+     */
+    public function buildDependencyInfo($depArray)
+    {
+        if (!is_array($depArray)) {
+            return new DependencyInfo(array(), array());
+        }
+        if (!$this->isHash($depArray)) {
+            return new DependencyInfo($this->buildDependency10Info($depArray), array());
+        }
+
+        return $this->buildDependency20Info($depArray);
+    }
+
+    /**
+     * Builds dependency information from package.xml 1.0 format
+     *
+     * http://pear.php.net/manual/en/guide.developers.package2.dependencies.php
+     *
+     * package.xml 1.0 format consists of array of
+     * { type="php|os|sapi|ext|pkg" rel="has|not|eq|ge|gt|le|lt" optional="yes"
+     *   channel="channelName" name="extName|packageName" }
+     *
+     * @param $depArray array Dependency data in package.xml 1.0 format
+     * @return DependencyConstraint[]
+     */
+    private function buildDependency10Info($depArray)
+    {
+        static $dep10toOperatorMap = array('has'=>'==', 'eq' => '==', 'ge' => '>=', 'gt' => '>', 'le' => '<=', 'lt' => '<', 'not' => '!=');
+
+        $result = array();
+
+        foreach ($depArray as $depItem) {
+            if (empty($depItem['rel']) || !array_key_exists($depItem['rel'], $dep10toOperatorMap)) {
+                // 'unknown rel type:' . $depItem['rel'];
+                continue;
+            }
+
+            $depType = !empty($depItem['optional']) && 'yes' == $depItem['optional']
+                ? 'optional'
+                : 'required';
+            $depType = 'not' == $depItem['rel']
+                ? 'conflicts'
+                : $depType;
+
+            $depVersion = !empty($depItem['version']) ? $this->parseVersion($depItem['version']) : '*';
+
+            // has & not are special operators that does not requires version
+            $depVersionConstraint = ('has' == $depItem['rel'] || 'not' == $depItem['rel']) && '*' == $depVersion
+                ? '*'
+                : $dep10toOperatorMap[$depItem['rel']] . $depVersion;
+
+            switch ($depItem['type']) {
+                case 'php':
+                    $depChannelName = 'php';
+                    $depPackageName = '';
+                    break;
+                case 'pkg':
+                    $depChannelName = !empty($depItem['channel']) ? $depItem['channel'] : 'pear.php.net';
+                    $depPackageName = $depItem['name'];
+                    break;
+                case 'ext':
+                    $depChannelName = 'ext';
+                    $depPackageName = $depItem['name'];
+                    break;
+                case 'os':
+                case 'sapi':
+                    $depChannelName = '';
+                    $depPackageName = '';
+                break;
+                default:
+                    $depChannelName = '';
+                    $depPackageName = '';
+                    break;
+            }
+
+            if ('' != $depChannelName) {
+                $result[] = new DependencyConstraint(
+                    $depType,
+                    $depVersionConstraint,
+                    $depChannelName,
+                    $depPackageName
+                );
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Builds dependency information from package.xml 2.0 format
+     *
+     * @param $depArray array Dependency data in package.xml 1.0 format
+     * @return DependencyInfo
+     */
+    private function buildDependency20Info($depArray)
+    {
+        $result = array();
+        $optionals = array();
+        $defaultOptionals = array();
+        foreach ($depArray as $depType => $depTypeGroup) {
+            if (!is_array($depTypeGroup)) {
+                continue;
+            }
+            if ('required' == $depType || 'optional' == $depType) {
+                foreach ($depTypeGroup as $depItemType => $depItem) {
+                    switch ($depItemType) {
+                        case 'php':
+                            $result[] = new DependencyConstraint(
+                                $depType,
+                                $this->parse20VersionConstraint($depItem),
+                                'php',
+                                ''
+                            );
+                            break;
+                        case 'package':
+                            $deps = $this->buildDepPackageConstraints($depItem, $depType);
+                            $result = array_merge($result, $deps);
+                            break;
+                        case 'extension':
+                            $deps = $this->buildDepExtensionConstraints($depItem, $depType);
+                            $result = array_merge($result, $deps);
+                            break;
+                        case 'subpackage':
+                            $deps = $this->buildDepPackageConstraints($depItem, 'replaces');
+                            $defaultOptionals += $deps;
+                            break;
+                        case 'os':
+                        case 'pearinstaller':
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            } elseif ('group' == $depType) {
+                if ($this->isHash($depTypeGroup)) {
+                    $depTypeGroup = array($depTypeGroup);
+                }
+
+                foreach ($depTypeGroup as $depItem) {
+                    $groupName = $depItem['attribs']['name'];
+                    if (!isset($optionals[$groupName])) {
+                        $optionals[$groupName] = array();
+                    }
+
+                    if (isset($depItem['subpackage'])) {
+                        $optionals[$groupName] += $this->buildDepPackageConstraints($depItem['subpackage'], 'replaces');
+                    } else {
+                        $result += $this->buildDepPackageConstraints($depItem['package'], 'optional');
+                    }
+                }
+            }
+        }
+
+        if (count($defaultOptionals) > 0) {
+            $optionals['*'] = $defaultOptionals;
+        }
+
+        return new DependencyInfo($result, $optionals);
+    }
+
+    /**
+     * Builds dependency constraint of 'extension' type
+     *
+     * @param $depItem array dependency constraint or array of dependency constraints
+     * @param $depType string target type of building constraint.
+     * @return DependencyConstraint[]
+     */
+    private function buildDepExtensionConstraints($depItem, $depType)
+    {
+        if ($this->isHash($depItem)) {
+            $depItem = array($depItem);
+        }
+
+        $result = array();
+        foreach ($depItem as $subDepItem) {
+            $depChannelName = 'ext';
+            $depPackageName = $subDepItem['name'];
+            $depVersionConstraint = $this->parse20VersionConstraint($subDepItem);
+
+            $result[] = new DependencyConstraint(
+                $depType,
+                $depVersionConstraint,
+                $depChannelName,
+                $depPackageName
+            );
+        }
+
+        return $result;
+    }
+
+    /**
+     * Builds dependency constraint of 'package' type
+     *
+     * @param $depItem array dependency constraint or array of dependency constraints
+     * @param $depType string target type of building constraint.
+     * @return DependencyConstraint[]
+     */
+    private function buildDepPackageConstraints($depItem, $depType)
+    {
+        if ($this->isHash($depItem)) {
+            $depItem = array($depItem);
+        }
+
+        $result = array();
+        foreach ($depItem as $subDepItem) {
+            $depChannelName = $subDepItem['channel'];
+            $depPackageName = $subDepItem['name'];
+            $depVersionConstraint = $this->parse20VersionConstraint($subDepItem);
+            if (isset($subDepItem['conflicts'])) {
+                $depType = 'conflicts';
+            }
+
+            $result[] = new DependencyConstraint(
+                $depType,
+                $depVersionConstraint,
+                $depChannelName,
+                $depPackageName
+            );
+        }
+
+        return $result;
+    }
+
+    /**
+     * Parses version constraint
+     *
+     * @param  array  $data array containing serveral 'min', 'max', 'has', 'exclude' and other keys.
+     * @return string
+     */
+    private function parse20VersionConstraint(array $data)
+    {
+        static $dep20toOperatorMap = array('has'=>'==', 'min' => '>=', 'max' => '<=', 'exclude' => '!=');
+
+        $versions = array();
+        $values = array_intersect_key($data, $dep20toOperatorMap);
+        if (0 == count($values)) {
+            return '*';
+        }
+        if (isset($values['min']) && isset($values['exclude']) && $data['min'] == $data['exclude']) {
+            $versions[] = '>' . $this->parseVersion($values['min']);
+        } elseif (isset($values['max']) && isset($values['exclude']) && $data['max'] == $data['exclude']) {
+            $versions[] = '<' . $this->parseVersion($values['max']);
+        } else {
+            foreach ($values as $op => $version) {
+                if ('exclude' == $op && is_array($version)) {
+                    foreach ($version as $versionPart) {
+                        $versions[] = $dep20toOperatorMap[$op] . $this->parseVersion($versionPart);
+                    }
+                } else {
+                    $versions[] = $dep20toOperatorMap[$op] . $this->parseVersion($version);
+                }
+            }
+        }
+
+        return implode(',', $versions);
+    }
+
+    /**
+     * Softened version parser
+     *
+     * @param $version
+     * @return null|string
+     */
+    private function parseVersion($version)
+    {
+        if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?}i', $version, $matches)) {
+            $version = $matches[1]
+                .(!empty($matches[2]) ? $matches[2] : '.0')
+                .(!empty($matches[3]) ? $matches[3] : '.0')
+                .(!empty($matches[4]) ? $matches[4] : '.0');
+
+            return $version;
+        }
+
+        return null;
+    }
+
+    /**
+     * Test if array is associative or hash type
+     *
+     * @param  array $array
+     * @return bool
+     */
+    private function isHash(array $array)
+    {
+        return !array_key_exists(1, $array) && !array_key_exists(0, $array);
+    }
+}

+ 94 - 0
src/Composer/Repository/Pear/PackageInfo.php

@@ -0,0 +1,94 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+/**
+ * PEAR Package info
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class PackageInfo
+{
+    private $channelName;
+    private $packageName;
+    private $license;
+    private $shortDescription;
+    private $description;
+    private $releases;
+
+    /**
+     * @param string        $channelName
+     * @param string        $packageName
+     * @param string        $license
+     * @param string        $shortDescription
+     * @param string        $description
+     * @param ReleaseInfo[] $releases         associative array maps release version to release info
+     */
+    public function __construct($channelName, $packageName, $license, $shortDescription, $description, $releases)
+    {
+        $this->channelName = $channelName;
+        $this->packageName = $packageName;
+        $this->license = $license;
+        $this->shortDescription = $shortDescription;
+        $this->description = $description;
+        $this->releases = $releases;
+    }
+
+    /**
+     * @return string the package channel name
+     */
+    public function getChannelName()
+    {
+        return $this->channelName;
+    }
+
+    /**
+     * @return string the package name
+     */
+    public function getPackageName()
+    {
+        return $this->packageName;
+    }
+
+    /**
+     * @return string the package description
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * @return string the package short escription
+     */
+    public function getShortDescription()
+    {
+        return $this->shortDescription;
+    }
+
+    /**
+     * @return string the package licence
+     */
+    public function getLicense()
+    {
+        return $this->license;
+    }
+
+    /**
+     * @return ReleaseInfo[]
+     */
+    public function getReleases()
+    {
+        return $this->releases;
+    }
+}

+ 50 - 0
src/Composer/Repository/Pear/ReleaseInfo.php

@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+/**
+ * PEAR package release info
+ *
+ * @author Alexey Prilipko <palex@farpost.com>
+ */
+class ReleaseInfo
+{
+    private $stability;
+    private $dependencyInfo;
+
+    /**
+     * @param string         $stability
+     * @param DependencyInfo $dependencies
+     */
+    public function __construct($stability, $dependencyInfo)
+    {
+        $this->stability = $stability;
+        $this->dependencyInfo = $dependencyInfo;
+    }
+
+    /**
+     * @return DependencyInfo release dependencies
+     */
+    public function getDependencyInfo()
+    {
+        return $this->dependencyInfo;
+    }
+
+    /**
+     * @return string release stability
+     */
+    public function getStability()
+    {
+        return $this->stability;
+    }
+}

+ 95 - 307
src/Composer/Repository/PearRepository.php

@@ -13,25 +13,35 @@
 namespace Composer\Repository;
 
 use Composer\IO\IOInterface;
-use Composer\Package\Loader\ArrayLoader;
+use Composer\Package\Version\VersionParser;
+use Composer\Repository\Pear\ChannelReader;
+use Composer\Package\MemoryPackage;
+use Composer\Repository\Pear\ChannelInfo;
+use Composer\Package\Link;
+use Composer\Package\LinkConstraint\VersionConstraint;
 use Composer\Util\RemoteFilesystem;
-use Composer\Json\JsonFile;
 use Composer\Config;
-use Composer\Downloader\TransportException;
 
 /**
+ * Builds list of package from PEAR channel.
+ *
+ * Packages read from channel are named as 'pear-{channelName}/{packageName}'
+ * and has aliased as 'pear-{channelAlias}/{packageName}'
+ *
  * @author Benjamin Eberlei <kontakt@beberlei.de>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
 class PearRepository extends ArrayRepository
 {
-    private static $channelNames = array();
-
     private $url;
-    private $baseUrl;
-    private $channel;
     private $io;
     private $rfs;
+    private $versionParser;
+
+    /** @var string vendor makes additional alias for each channel as {prefix}/{packagename}. It allows smoother
+     * package transition to composer-like repositories.
+     */
+    private $vendorAlias;
 
     public function __construct(array $repoConfig, IOInterface $io, Config $config, RemoteFilesystem $rfs = null)
     {
@@ -44,9 +54,10 @@ class PearRepository extends ArrayRepository
         }
 
         $this->url = rtrim($repoConfig['url'], '/');
-        $this->channel = !empty($repoConfig['channel']) ? $repoConfig['channel'] : null;
         $this->io = $io;
         $this->rfs = $rfs ?: new RemoteFilesystem($this->io);
+        $this->vendorAlias = isset($repoConfig['vendor-alias']) ? $repoConfig['vendor-alias'] : null;
+        $this->versionParser = new VersionParser();
     }
 
     protected function initialize()
@@ -54,341 +65,118 @@ class PearRepository extends ArrayRepository
         parent::initialize();
 
         $this->io->write('Initializing PEAR repository '.$this->url);
-        $this->initializeChannel();
-        $this->io->write('Packages names will be prefixed with: pear-'.$this->channel.'/');
 
-        // try to load as a composer repo
+        $reader = new ChannelReader($this->rfs);
         try {
-            $json     = new JsonFile($this->url.'/packages.json', new RemoteFilesystem($this->io));
-            $packages = $json->read();
-
-            if ($this->io->isVerbose()) {
-                $this->io->write('Repository is Composer-compatible, loading via packages.json instead of PEAR protocol');
-            }
-
-            $loader = new ArrayLoader();
-            foreach ($packages as $data) {
-                foreach ($data['versions'] as $rev) {
-                    if (strpos($rev['name'], 'pear-'.$this->channel) !== 0) {
-                        $rev['name'] = 'pear-'.$this->channel.'/'.$rev['name'];
-                    }
-                    $this->addPackage($loader->load($rev));
-                    if ($this->io->isVerbose()) {
-                        $this->io->write('Loaded '.$rev['name'].' '.$rev['version']);
-                    }
-                }
-            }
-
-            return;
+            $channelInfo = $reader->read($this->url);
         } catch (\Exception $e) {
-        }
-
-        $this->fetchFromServer();
-    }
+            $this->io->write('<warning>PEAR repository from '.$this->url.' could not be loaded. '.$e->getMessage().'</warning>');
 
-    protected function initializeChannel()
-    {
-        $channelXML = $this->requestXml($this->url . "/channel.xml");
-        if (!$this->channel) {
-            $this->channel = $channelXML->getElementsByTagName("suggestedalias")->item(0)->nodeValue
-                                    ?: $channelXML->getElementsByTagName("name")->item(0)->nodeValue;
-        }
-        if (!$this->baseUrl) {
-            $this->baseUrl = $channelXML->getElementsByTagName("baseurl")->item(0)->nodeValue
-                                    ? trim($channelXML->getElementsByTagName("baseurl")->item(0)->nodeValue, '/')
-                                    : $this->url . '/rest';
+            return;
         }
-
-        self::$channelNames[$channelXML->getElementsByTagName("name")->item(0)->nodeValue] = $this->channel;
-    }
-
-    protected function fetchFromServer()
-    {
-        $categoryXML = $this->requestXml($this->baseUrl . "/c/categories.xml");
-        $categories = $categoryXML->getElementsByTagName("c");
-
-        foreach ($categories as $category) {
-            $link = $this->baseUrl . '/c/' . str_replace(' ', '+', $category->nodeValue);
-            try {
-                $packagesLink = $link . "/packagesinfo.xml";
-                $this->fetchPear2Packages($packagesLink);
-            } catch (TransportException $e) {
-                if (false === strpos($e->getMessage(), '404')) {
-                    throw $e;
-                }
-                $categoryLink = $link . "/packages.xml";
-                $this->fetchPearPackages($categoryLink);
-            }
-
+        $packages = $this->buildComposerPackages($channelInfo, $this->versionParser);
+        foreach ($packages as $package) {
+            $this->addPackage($package);
         }
     }
 
     /**
-     * @param  string                    $categoryLink
-     * @throws TransportException
-     * @throws \InvalidArgumentException
+     * Builds MemoryPackages from PEAR package definition data.
+     *
+     * @param  ChannelInfo   $channelInfo
+     * @return MemoryPackage
      */
-    private function fetchPearPackages($categoryLink)
+    private function buildComposerPackages(ChannelInfo $channelInfo, VersionParser $versionParser)
     {
-        $packagesXML = $this->requestXml($categoryLink);
-        $packages = $packagesXML->getElementsByTagName('p');
-        $loader = new ArrayLoader();
-        foreach ($packages as $package) {
-            $packageName = $package->nodeValue;
-            $fullName = 'pear-'.$this->channel.'/'.$packageName;
-
-            $releaseLink = $this->baseUrl . "/r/" . $packageName;
-            $allReleasesLink = $releaseLink . "/allreleases2.xml";
-
-            try {
-                $releasesXML = $this->requestXml($allReleasesLink);
-            } catch (TransportException $e) {
-                if (strpos($e->getMessage(), '404')) {
-                    continue;
-                }
-                throw $e;
-            }
-
-            $releases = $releasesXML->getElementsByTagName('r');
-
-            foreach ($releases as $release) {
-                /* @var $release \DOMElement */
-                $pearVersion = $release->getElementsByTagName('v')->item(0)->nodeValue;
-
-                $packageData = array(
-                    'name' => $fullName,
-                    'type' => 'library',
-                    'dist' => array('type' => 'pear', 'url' => $this->url.'/get/'.$packageName.'-'.$pearVersion.".tgz"),
-                    'version' => $pearVersion,
-                    'autoload' => array(
-                        'classmap' => array(''),
-                    ),
-                    'include-path' => array('/'),
-                );
-
+        $result = array();
+        foreach ($channelInfo->getPackages() as $packageDefinition) {
+            foreach ($packageDefinition->getReleases() as $version => $releaseInfo) {
                 try {
-                    $deps = $this->rfs->getContents($this->url, $releaseLink . "/deps.".$pearVersion.".txt", false);
-                } catch (TransportException $e) {
-                    if (strpos($e->getMessage(), '404')) {
-                        continue;
-                    }
-                    throw $e;
-                }
-
-                $packageData += $this->parseDependencies($deps);
-
-                try {
-                    $this->addPackage($loader->load($packageData));
-                    if ($this->io->isVerbose()) {
-                        $this->io->write('Loaded '.$packageData['name'].' '.$packageData['version']);
-                    }
+                    $normalizedVersion = $versionParser->normalize($version);
                 } catch (\UnexpectedValueException $e) {
                     if ($this->io->isVerbose()) {
-                        $this->io->write('Could not load '.$packageData['name'].' '.$packageData['version'].': '.$e->getMessage());
+                        $this->io->write('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage());
                     }
                     continue;
                 }
-            }
-        }
-    }
 
-    /**
-     * @param  array  $data
-     * @return string
-     */
-    private function parseVersion(array $data)
-    {
-        if (!isset($data['min']) && !isset($data['max'])) {
-            return '*';
-        }
-        $versions = array();
-        if (isset($data['min'])) {
-            $versions[] = '>=' . $data['min'];
-        }
-        if (isset($data['max'])) {
-            $versions[] = '<=' . $data['max'];
-        }
+                $composerPackageName = $this->buildComposerPackageName($packageDefinition->getChannelName(), $packageDefinition->getPackageName());
 
-        return implode(',', $versions);
-    }
+                // distribution url must be read from /r/{packageName}/{version}.xml::/r/g:text()
+                // but this location is 'de-facto' standard
+                $distUrl = "http://{$packageDefinition->getChannelName()}/get/{$packageDefinition->getPackageName()}-{$version}.tgz";
 
-    /**
-     * @todo    Improve dependencies resolution of pear packages.
-     * @param  array $depsOptions
-     * @return array
-     */
-    private function parseDependenciesOptions(array $depsOptions)
-    {
-        $data = array();
-        foreach ($depsOptions as $name => $options) {
-            // make sure single deps are wrapped in an array
-            if (isset($options['name'])) {
-                $options = array($options);
-            }
-            if ('php' == $name) {
-                $data[$name] = $this->parseVersion($options);
-            } elseif ('package' == $name) {
-                foreach ($options as $key => $value) {
-                    if (isset($value['providesextension'])) {
-                        // skip PECL dependencies
-                        continue;
-                    }
-                    if (isset($value['uri'])) {
-                        // skip uri-based dependencies
-                        continue;
-                    }
+                $requires = array();
+                $suggests = array();
+                $conflicts = array();
+                $replaces = array();
 
-                    if (is_array($value)) {
-                        $dataKey = $value['name'];
-                        if (false === strpos($dataKey, '/')) {
-                            $dataKey = $this->getChannelShorthand($value['channel']).'/'.$dataKey;
-                        }
-                        $data['pear-'.$dataKey] = $this->parseVersion($value);
-                    }
-                }
-            } elseif ('extension' == $name) {
-                foreach ($options as $key => $value) {
-                    $dataKey = 'ext-' . $value['name'];
-                    $data[$dataKey] = $this->parseVersion($value);
+                // alias package only when its channel matches repository channel,
+                // cause we've know only repository channel alias
+                if ($channelInfo->getName() == $packageDefinition->getChannelName()) {
+                    $composerPackageAlias = $this->buildComposerPackageName($channelInfo->getAlias(), $packageDefinition->getPackageName());
+                    $aliasConstraint = new VersionConstraint('==', $normalizedVersion);
+                    $replaces[] = new Link($composerPackageName, $composerPackageAlias, $aliasConstraint, 'replaces', (string) $aliasConstraint);
                 }
-            }
-        }
-
-        return $data;
-    }
-
-    /**
-     * @param  string                    $deps
-     * @return array
-     * @throws \InvalidArgumentException
-     */
-    private function parseDependencies($deps)
-    {
-        if (preg_match('((O:([0-9])+:"([^"]+)"))', $deps, $matches)) {
-            if (strlen($matches[3]) == $matches[2]) {
-                throw new \InvalidArgumentException("Invalid dependency data, it contains serialized objects.");
-            }
-        }
-        $deps = (array) @unserialize($deps);
-        unset($deps['required']['pearinstaller']);
-
-        $depsData = array();
-        if (!empty($deps['required'])) {
-            $depsData['require'] = $this->parseDependenciesOptions($deps['required']);
-        }
-
-        if (!empty($deps['optional'])) {
-            $depsData['suggest'] = $this->parseDependenciesOptions($deps['optional']);
-        }
-
-        return $depsData;
-    }
 
-    /**
-     * @param  string                    $packagesLink
-     * @return void
-     * @throws \InvalidArgumentException
-     */
-    private function fetchPear2Packages($packagesLink)
-    {
-        $loader = new ArrayLoader();
-        $packagesXml = $this->requestXml($packagesLink);
-
-        $informations = $packagesXml->getElementsByTagName('pi');
-        foreach ($informations as $information) {
-            $package = $information->getElementsByTagName('p')->item(0);
-
-            $packageName = $package->getElementsByTagName('n')->item(0)->nodeValue;
-            $fullName = 'pear-'.$this->channel.'/'.$packageName;
-            $packageData = array(
-                'name' => $fullName,
-                'type' => 'library',
-                'autoload' => array(
-                    'classmap' => array(''),
-                ),
-                'include-path' => array('/'),
-            );
-            $packageKeys = array('l' => 'license', 'd' => 'description');
-            foreach ($packageKeys as $pear => $composer) {
-                if ($package->getElementsByTagName($pear)->length > 0
-                        && ($pear = $package->getElementsByTagName($pear)->item(0)->nodeValue)) {
-                    $packageData[$composer] = $pear;
+                // alias package with user-specified prefix. it makes private pear channels looks like composer's.
+                if (!empty($this->vendorAlias)) {
+                    $composerPackageAlias = "{$this->vendorAlias}/{$packageDefinition->getPackageName()}";
+                    $aliasConstraint = new VersionConstraint('==', $normalizedVersion);
+                    $replaces[] = new Link($composerPackageName, $composerPackageAlias, $aliasConstraint, 'replaces', (string) $aliasConstraint);
                 }
-            }
 
-            $depsData = array();
-            foreach ($information->getElementsByTagName('deps') as $depElement) {
-                $depsVersion = $depElement->getElementsByTagName('v')->item(0)->nodeValue;
-                $depsData[$depsVersion] = $this->parseDependencies(
-                    $depElement->getElementsByTagName('d')->item(0)->nodeValue
-                );
-            }
-
-            $releases = $information->getElementsByTagName('a')->item(0);
-            if (!$releases) {
-                continue;
-            }
-
-            $releases = $releases->getElementsByTagName('r');
-            $packageUrl = $this->url . '/get/' . $packageName;
-            foreach ($releases as $release) {
-                $version = $release->getElementsByTagName('v')->item(0)->nodeValue;
-                $releaseData = array(
-                    'dist' => array(
-                        'type' => 'pear',
-                        'url' => $packageUrl . '-' . $version . '.tgz'
-                    ),
-                    'version' => $version
-                );
-                if (isset($depsData[$version])) {
-                    $releaseData += $depsData[$version];
+                foreach ($releaseInfo->getDependencyInfo()->getRequires() as $dependencyConstraint) {
+                    $dependencyPackageName = $this->buildComposerPackageName($dependencyConstraint->getChannelName(), $dependencyConstraint->getPackageName());
+                    $constraint = $versionParser->parseConstraints($dependencyConstraint->getConstraint());
+                    $link = new Link($composerPackageName, $dependencyPackageName, $constraint, $dependencyConstraint->getType(), $dependencyConstraint->getConstraint());
+                    switch ($dependencyConstraint->getType()) {
+                        case 'required':
+                            $requires[] = $link;
+                            break;
+                        case 'conflicts':
+                            $conflicts[] = $link;
+                            break;
+                        case 'replaces':
+                            $replaces[] = $link;
+                            break;
+                    }
                 }
 
-                $package = $packageData + $releaseData;
-                try {
-                    $this->addPackage($loader->load($package));
-                    if ($this->io->isVerbose()) {
-                        $this->io->write('Loaded '.$package['name'].' '.$package['version']);
+                foreach ($releaseInfo->getDependencyInfo()->getOptionals() as $group => $dependencyConstraints) {
+                    foreach ($dependencyConstraints as $dependencyConstraint) {
+                        $dependencyPackageName = $this->buildComposerPackageName($dependencyConstraint->getChannelName(), $dependencyConstraint->getPackageName());
+                        $suggests[$group.'-'.$dependencyPackageName] = $dependencyConstraint->getConstraint();
                     }
-                } catch (\UnexpectedValueException $e) {
-                    if ($this->io->isVerbose()) {
-                        $this->io->write('Could not load '.$package['name'].' '.$package['version'].': '.$e->getMessage());
-                    }
-                    continue;
                 }
-            }
-        }
-    }
 
-    /**
-     * @param  string       $url
-     * @return \DOMDocument
-     */
-    private function requestXml($url)
-    {
-        $content = $this->rfs->getContents($this->url, $url, false);
-        if (!$content) {
-            throw new \UnexpectedValueException('The PEAR channel at '.$url.' did not respond.');
+                $package = new MemoryPackage($composerPackageName, $normalizedVersion, $version);
+                $package->setType('pear-library');
+                $package->setDescription($packageDefinition->getDescription());
+                $package->setDistType('file');
+                $package->setDistUrl($distUrl);
+                $package->setAutoload(array('classmap' => array('')));
+                $package->setIncludePaths(array('/'));
+                $package->setRequires($requires);
+                $package->setConflicts($conflicts);
+                $package->setSuggests($suggests);
+                $package->setReplaces($replaces);
+                $result[] = $package;
+            }
         }
-        $dom = new \DOMDocument('1.0', 'UTF-8');
-        $dom->loadXML($content);
 
-        return $dom;
+        return $result;
     }
 
-    private function getChannelShorthand($url)
+    private function buildComposerPackageName($channelName, $packageName)
     {
-        if (!isset(self::$channelNames[$url])) {
-            try {
-                $channelXML = $this->requestXml('http://'.$url."/channel.xml");
-                $shorthand = $channelXML->getElementsByTagName("suggestedalias")->item(0)->nodeValue
-                    ?: $channelXML->getElementsByTagName("name")->item(0)->nodeValue;
-                self::$channelNames[$url] = $shorthand;
-            } catch (\Exception $e) {
-                self::$channelNames[$url] = substr($url, 0, strpos($url, '.'));
-            }
+        if ('php' === $channelName) {
+            return "php";
+        }
+        if ('ext' === $channelName) {
+            return "ext-{$packageName}";
         }
 
-        return self::$channelNames[$url];
+        return "pear-{$channelName}/{$packageName}";
     }
 }

+ 11 - 1
tests/Composer/Test/Downloader/Fixtures/Package_v2.1/package.xml

@@ -19,7 +19,17 @@
         <dir name="/">
             <file role="php" name="php/Zend/Authentication/Storage/StorageInterface.php"/>
             <file role="php" name="php/Zend/Authentication/Result.php"/>
+            <file role="script" name="php/Test.php">
+                <tasks:replace from='@version@' to='version' />
+            </file>
+            <file role="php" name="renamedFile.php"/>
+            <file role="php" name="ignoredFile.php"/>
         </dir>
     </contents>
-    <phprelease/>
+    <phprelease>
+        <filelist>
+            <install name='renamedFile.php' as='correctFile.php' />
+            <ignore name='ignoredFile.php' />
+        </filelist>
+    </phprelease>
 </package>

+ 0 - 38
tests/Composer/Test/Downloader/PearDownloaderTest.php

@@ -1,38 +0,0 @@
-<?php
-
-/*
- * This file is part of Composer.
- *
- * (c) Nils Adermann <naderman@naderman.de>
- *     Jordi Boggiano <j.boggiano@seld.be>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Composer\Test\Downloader;
-
-use Composer\Downloader\PearDownloader;
-
-class PearDownloaderTest extends \PHPUnit_Framework_TestCase
-{
-    public function testErrorMessages()
-    {
-        $packageMock = $this->getMock('Composer\Package\PackageInterface');
-        $packageMock->expects($this->any())
-            ->method('getDistUrl')
-            ->will($this->returnValue('file://'.__FILE__))
-        ;
-
-        $io = $this->getMock('Composer\IO\IOInterface');
-        $downloader = new PearDownloader($io);
-
-        try {
-            $downloader->download($packageMock, sys_get_temp_dir().'/composer-pear-test');
-            $this->fail('Download of invalid pear packages should throw an exception');
-        } catch (\UnexpectedValueException $e) {
-            $this->assertContains('Failed to extract PEAR package', $e->getMessage());
-        }
-    }
-
-}

+ 62 - 10
tests/Composer/Test/Downloader/PearPackageExtractorTest.php

@@ -22,20 +22,26 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase
         $method = new \ReflectionMethod($extractor, 'buildCopyActions');
         $method->setAccessible(true);
 
-        $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v1.0', 'php');
+        $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v1.0', array('php' => '/'), array());
 
         $expectedFileActions = array(
-            0 => Array(
+            'Gtk.php' => Array(
                 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk.php',
                 'to' => 'PEAR/Frontend/Gtk.php',
+                'role' => 'php',
+                'tasks' => array(),
             ),
-            1 => Array(
+            'Gtk/Config.php' => Array(
                 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk/Config.php',
                 'to' => 'PEAR/Frontend/Gtk/Config.php',
+                'role' => 'php',
+                'tasks' => array(),
             ),
-            2 => Array(
+            'Gtk/xpm/black_close_icon.xpm' => Array(
                 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk/xpm/black_close_icon.xpm',
                 'to' => 'PEAR/Frontend/Gtk/xpm/black_close_icon.xpm',
+                'role' => 'php',
+                'tasks' => array(),
             )
         );
         $this->assertSame($expectedFileActions, $fileActions);
@@ -47,12 +53,14 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase
         $method = new \ReflectionMethod($extractor, 'buildCopyActions');
         $method->setAccessible(true);
 
-        $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v2.0', 'php');
+        $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v2.0', array('php' => '/'), array());
 
         $expectedFileActions = array(
-            0 => Array(
+            'URL.php' => Array(
                 'from' => 'Net_URL-1.0.15/URL.php',
                 'to' => 'Net/URL.php',
+                'role' => 'php',
+                'tasks' => array(),
             )
         );
         $this->assertSame($expectedFileActions, $fileActions);
@@ -64,18 +72,62 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase
         $method = new \ReflectionMethod($extractor, 'buildCopyActions');
         $method->setAccessible(true);
 
-        $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v2.1', 'php');
+        $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v2.1', array('php' => '/', 'script' => '/bin'), array());
 
         $expectedFileActions = array(
-            0 => Array(
+            'php/Zend/Authentication/Storage/StorageInterface.php' => Array(
                 'from' => 'Zend_Authentication-2.0.0beta4/php/Zend/Authentication/Storage/StorageInterface.php',
                 'to' => '/php/Zend/Authentication/Storage/StorageInterface.php',
+                'role' => 'php',
+                'tasks' => array(),
             ),
-            1 => Array(
+            'php/Zend/Authentication/Result.php' => Array(
                 'from' => 'Zend_Authentication-2.0.0beta4/php/Zend/Authentication/Result.php',
                 'to' => '/php/Zend/Authentication/Result.php',
-            )
+                'role' => 'php',
+                'tasks' => array(),
+            ),
+            'php/Test.php' => array (
+                'from' => 'Zend_Authentication-2.0.0beta4/php/Test.php',
+                'to' => '/php/Test.php',
+                'role' => 'script',
+                'tasks' => array (
+                    array (
+                        'from' => '@version@',
+                        'to' => 'version',
+                    )
+                )
+            ),
+            'renamedFile.php' => Array(
+                'from' => 'Zend_Authentication-2.0.0beta4/renamedFile.php',
+                'to' => 'correctFile.php',
+                'role' => 'php',
+                'tasks' => array(),
+            ),
         );
         $this->assertSame($expectedFileActions, $fileActions);
     }
+
+    public function testShouldPerformReplacements()
+    {
+        $from = tempnam(sys_get_temp_dir(), 'pear-extract');
+        $to = $from.'-to';
+
+        $original = 'replaced: @placeholder@; not replaced: @another@; replaced again: @placeholder@';
+        $expected = 'replaced: value; not replaced: @another@; replaced again: value';
+
+        file_put_contents($from, $original);
+
+        $extractor = new PearPackageExtractor($from);
+        $method = new \ReflectionMethod($extractor, 'copyFile');
+        $method->setAccessible(true);
+
+        $method->invoke($extractor, $from, $to, array(array('from' => '@placeholder@', 'to' => 'variable')), array('variable' => 'value'));
+        $result = file_get_contents($to);
+
+        unlink($to);
+        unlink($from);
+
+        $this->assertEquals($expected, $result);
+    }
 }

+ 3 - 3
tests/Composer/Test/Installer/LibraryInstallerTest.php

@@ -106,7 +106,7 @@ class LibraryInstallerTest extends TestCase
         $package = $this->createPackageMock();
 
         $package
-            ->expects($this->once())
+            ->expects($this->any())
             ->method('getPrettyName')
             ->will($this->returnValue('some/package'));
 
@@ -136,7 +136,7 @@ class LibraryInstallerTest extends TestCase
         $target  = $this->createPackageMock();
 
         $initial
-            ->expects($this->once())
+            ->expects($this->any())
             ->method('getPrettyName')
             ->will($this->returnValue('package1'));
 
@@ -175,7 +175,7 @@ class LibraryInstallerTest extends TestCase
         $package = $this->createPackageMock();
 
         $package
-            ->expects($this->once())
+            ->expects($this->any())
             ->method('getPrettyName')
             ->will($this->returnValue('pkg'));
 

+ 40 - 0
tests/Composer/Test/Mock/RemoteFilesystemMock.php

@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Mock;
+
+use Composer\Util\RemoteFilesystem;
+use Composer\Downloader\TransportException;
+
+/**
+ * Remote filesystem mock
+ */
+class RemoteFilesystemMock extends RemoteFilesystem
+{
+    /**
+     * @param array $contentMap associative array of locations and content
+     */
+    public function __construct(array $contentMap)
+    {
+        $this->contentMap = $contentMap;
+    }
+
+    public function getContents($originUrl, $fileUrl, $progress = true)
+    {
+        if (!empty($this->contentMap[$fileUrl])) {
+            return $this->contentMap[$fileUrl];
+        }
+
+        throw new TransportException('The "'.$fileUrl.'" file could not be downloaded (NOT FOUND)', 404);
+    }
+
+}

+ 151 - 0
tests/Composer/Test/Repository/Pear/ChannelReaderTest.php

@@ -0,0 +1,151 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+use Composer\Test\TestCase;
+use Composer\Package\Version\VersionParser;
+use Composer\Package\LinkConstraint\VersionConstraint;
+use Composer\Package\Link;
+use Composer\Package\MemoryPackage;
+use Composer\Test\Mock\RemoteFilesystemMock;
+
+class ChannelReaderTest extends TestCase
+{
+    public function testShouldBuildPackagesFromPearSchema()
+    {
+        $rfs = new RemoteFilesystemMock(array(
+            'http://pear.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'),
+            'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'),
+            'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'),
+        ));
+
+        $reader = new \Composer\Repository\Pear\ChannelReader($rfs);
+
+        $channelInfo = $reader->read('http://pear.net/');
+        $packages = $channelInfo->getPackages();
+
+        $this->assertCount(3, $packages);
+        $this->assertEquals('HTTP_Client', $packages[0]->getPackageName());
+        $this->assertEquals('HTTP_Request', $packages[1]->getPackageName());
+        $this->assertEquals('MDB2', $packages[2]->getPackageName());
+
+        $mdb2releases = $packages[2]->getReleases();
+        $this->assertEquals(9, count($mdb2releases['2.4.0']->getDependencyInfo()->getOptionals()));
+    }
+
+    public function testShouldSelectCorrectReader()
+    {
+        $rfs = new RemoteFilesystemMock(array(
+            'http://pear.1.0.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.0.xml'),
+            'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'),
+            'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'),
+            'http://test.loc/rest10/p/http_request/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_info.xml'),
+            'http://pear.1.1.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'),
+            'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'),
+            'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'),
+        ));
+
+        $reader = new \Composer\Repository\Pear\ChannelReader($rfs);
+
+        $reader->read('http://pear.1.0.net/');
+        $reader->read('http://pear.1.1.net/');
+    }
+
+    public function testShouldCreatePackages()
+    {
+        $reader = $this->getMockBuilder('\Composer\Repository\PearRepository')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $ref = new \ReflectionMethod($reader, 'buildComposerPackages');
+        $ref->setAccessible(true);
+
+        $channelInfo = new ChannelInfo(
+            'test.loc',
+            'test',
+            array(
+                new PackageInfo(
+                    'test.loc',
+                    'sample',
+                    'license',
+                    'shortDescription',
+                    'description',
+                    array(
+                        '1.0.0.1' => new ReleaseInfo(
+                            'stable',
+                            new DependencyInfo(
+                                array(
+                                    new DependencyConstraint(
+                                        'required',
+                                        '> 5.2.0.0',
+                                        'php',
+                                        ''
+                                    ),
+                                    new DependencyConstraint(
+                                        'conflicts',
+                                        '== 2.5.6.0',
+                                        'pear.php.net',
+                                        'broken'
+                                    ),
+                                ),
+                                array(
+                                    '*' => array(
+                                        new DependencyConstraint(
+                                            'optional',
+                                            '*',
+                                            'ext',
+                                            'xml'
+                                        ),
+                                    )
+                                )
+                            )
+                        )
+                    )
+                )
+            )
+        );
+
+        $packages = $ref->invoke($reader, $channelInfo, new VersionParser());
+
+        $expectedPackage = new MemoryPackage('pear-test.loc/sample', '1.0.0.1' , '1.0.0.1');
+        $expectedPackage->setType('pear-library');
+        $expectedPackage->setDistType('file');
+        $expectedPackage->setDescription('description');
+        $expectedPackage->setDistUrl("http://test.loc/get/sample-1.0.0.1.tgz");
+        $expectedPackage->setAutoload(array('classmap' => array('')));
+        $expectedPackage->setIncludePaths(array('/'));
+        $expectedPackage->setRequires(array(
+            new Link('pear-test.loc/sample', 'php', $this->createConstraint('>', '5.2.0.0'), 'required', '> 5.2.0.0'),
+        ));
+        $expectedPackage->setConflicts(array(
+            new Link('pear-test.loc/sample', 'pear-pear.php.net/broken', $this->createConstraint('==', '2.5.6.0'), 'conflicts', '== 2.5.6.0'),
+        ));
+        $expectedPackage->setSuggests(array(
+            '*-ext-xml' => '*',
+        ));
+        $expectedPackage->setReplaces(array(
+            new Link('pear-test.loc/sample', 'pear-test/sample', new VersionConstraint('==', '1.0.0.1'), 'replaces', '== 1.0.0.1'),
+        ));
+
+        $this->assertCount(1, $packages);
+        $this->assertEquals($expectedPackage, $packages[0], 0, 1);
+    }
+
+    private function createConstraint($operator, $version)
+    {
+        $constraint = new VersionConstraint($operator, $version);
+        $constraint->setPrettyString($operator.' '.$version);
+
+        return $constraint;
+    }
+}

+ 41 - 0
tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php

@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+use Composer\Test\TestCase;
+use Composer\Test\Mock\RemoteFilesystemMock;
+
+class ChannelRest10ReaderTest extends TestCase
+{
+    public function testShouldBuildPackagesFromPearSchema()
+    {
+        $rfs = new RemoteFilesystemMock(array(
+            'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'),
+            'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'),
+            'http://test.loc/rest10/r/http_client/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_allreleases.xml'),
+            'http://test.loc/rest10/r/http_client/deps.1.2.1.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_deps.1.2.1.txt'),
+            'http://test.loc/rest10/p/http_request/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_info.xml'),
+            'http://test.loc/rest10/r/http_request/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_allreleases.xml'),
+            'http://test.loc/rest10/r/http_request/deps.1.4.0.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_deps.1.4.0.txt'),
+        ));
+
+        $reader = new \Composer\Repository\Pear\ChannelRest10Reader($rfs);
+
+        /** @var $packages \Composer\Package\PackageInterface[] */
+        $packages = $reader->read('http://test.loc/rest10');
+
+        $this->assertCount(2, $packages);
+        $this->assertEquals('HTTP_Client', $packages[0]->getPackageName());
+        $this->assertEquals('HTTP_Request', $packages[1]->getPackageName());
+    }
+}

+ 37 - 0
tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php

@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+use Composer\Test\TestCase;
+use Composer\Test\Mock\RemoteFilesystemMock;
+
+class ChannelRest11ReaderTest extends TestCase
+{
+    public function testShouldBuildPackagesFromPearSchema()
+    {
+        $rfs = new RemoteFilesystemMock(array(
+            'http://pear.1.1.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'),
+            'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'),
+            'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'),
+        ));
+
+        $reader = new \Composer\Repository\Pear\ChannelRest11Reader($rfs);
+
+        /** @var $packages \Composer\Package\PackageInterface[] */
+        $packages = $reader->read('http://test.loc/rest11');
+
+        $this->assertCount(3, $packages);
+        $this->assertEquals('HTTP_Client', $packages[0]->getPackageName());
+        $this->assertEquals('HTTP_Request', $packages[1]->getPackageName());
+    }
+}

+ 167 - 0
tests/Composer/Test/Repository/Pear/Fixtures/DependencyParserTestData.json

@@ -0,0 +1,167 @@
+[
+    {
+        "expected": [
+            {
+                "type" : "required",
+                "constraint" : "*",
+                "channel" : "pear.php.net",
+                "name" : "Foo"
+            }
+        ],
+        "1.0": [
+            { "type": "pkg", "rel": "has", "name": "Foo" }
+        ],
+        "2.0": {
+            "required": {
+                "package": {
+                    "name": "Foo",
+                    "channel": "pear.php.net"
+                }
+            }
+        }
+    },
+    {
+        "expected": [
+            {
+                "type" : "required",
+                "constraint" : ">1.0.0.0",
+                "channel" : "pear.php.net",
+                "name" : "Foo"
+            }
+        ],
+        "1.0": [
+            { "type": "pkg", "rel": "gt", "version": "1.0.0", "name": "Foo" }
+        ],
+        "2.0": {
+            "required": {
+                "package": {
+                    "name": "Foo",
+                    "channel": "pear.php.net",
+                    "min": "1.0.0",
+                    "exclude": "1.0.0"
+                }
+            }
+        }
+    },
+    {
+        "expected": [
+            {
+                "type" : "conflicts",
+                "constraint" : "*",
+                "channel" : "pear.php.net",
+                "name" : "Foo"
+            }
+        ],
+        "1.0": [
+            { "type": "pkg", "rel": "not", "name": "Foo" }
+        ],
+        "2.0": {
+            "required": {
+                "package": {
+                    "name": "Foo",
+                    "channel": "pear.php.net",
+                    "conflicts": true
+                }
+            }
+        }
+    },
+    {
+        "expected": [
+            {
+                "type" : "required",
+                "constraint" : ">=1.0.0.0",
+                "channel" : "pear.php.net",
+                "name" : "Foo"
+            },
+            {
+                "type" : "required",
+                "constraint" : "<2.0.0.0",
+                "channel" : "pear.php.net",
+                "name" : "Foo"
+            }
+        ],
+        "1.0": [
+            { "type": "pkg", "rel": "ge", "version": "1.0.0", "name": "Foo" },
+            { "type": "pkg", "rel": "lt", "version": "2.0.0", "name": "Foo" }
+        ],
+        "2.0": {
+            "required": {
+                "package": [
+                    {
+                        "name": "Foo",
+                        "channel": "pear.php.net",
+                        "min": "1.0.0"
+                    },
+                    {
+                        "name": "Foo",
+                        "channel": "pear.php.net",
+                        "max": "2.0.0",
+                        "exclude": "2.0.0"
+                    }
+                ]
+            }
+        }
+    },
+    {
+        "expected": [
+            {
+                "type" : "required",
+                "constraint" : ">=5.3.0.0",
+                "channel" : "php",
+                "name" : ""
+            }
+        ],
+        "1.0": [
+            { "type": "php", "rel": "ge", "version": "5.3"}
+        ],
+        "2.0": {
+            "required": {
+                "php": {
+                        "min": "5.3"
+                }
+            }
+        }
+    },
+    {
+        "expected": [
+            {
+                "type" : "required",
+                "constraint" : "*",
+                "channel" : "ext",
+                "name" : "xmllib"
+            }
+        ],
+        "1.0": [
+            { "type": "ext", "rel": "has", "name": "xmllib"}
+        ],
+        "2.0": {
+            "required": {
+                "extension": [
+                    {
+                        "name": "xmllib"
+                    }
+                ]
+            }
+        }
+    },
+    {
+        "expected": [
+            {
+                "type" : "optional",
+                "constraint" : "*",
+                "channel" : "ext",
+                "name" : "xmllib"
+            }
+        ],
+        "1.0": false,
+        "2.0": {
+            "optional": {
+                "extension": [
+                    {
+                        "name": "xmllib"
+                    }
+                ]
+            }
+        }
+    }
+]

+ 9 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_allreleases.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<a xmlns="http://pear.php.net/dtd/rest.allreleases" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.allreleases http://pear.php.net/dtd/rest.allreleases.xsd">
+    <p>HTTP_Client</p>
+    <c>pear.net</c>
+    <r>
+        <v>1.2.1</v>
+        <s>stable</s>
+    </r>
+</a>

+ 1 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_deps.1.2.1.txt

@@ -0,0 +1 @@
+a:1:{s:8:"required";a:3:{s:3:"php";a:1:{s:3:"min";s:5:"4.3.0";}s:13:"pearinstaller";a:1:{s:3:"min";s:5:"1.4.3";}s:7:"package";a:3:{s:4:"name";s:12:"HTTP_Request";s:7:"channel";s:8:"pear.net";s:3:"min";s:5:"1.4.0";}}}

+ 14 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_info.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<p xmlns="http://pear.php.net/dtd/rest.package" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.package http://pear.php.net/dtd/rest.package.xsd">
+    <n>HTTP_Client</n>
+    <c>pear.net</c>
+    <ca xlink:href="/rest/c/Default">Default</ca>
+    <l>BSD</l>
+    <s>
+        Easy way to perform multiple HTTP requests and process their results
+    </s>
+    <d>
+        The HTTP_Client class wraps around HTTP_Request and provides a higher level interface for performing multiple HTTP requests. Features: * Manages cookies and referrers between requests * Handles HTTP redirection * Has methods to set default headers and request parameters * Implements the Subject-Observer design pattern: the base class sends events to listeners that do the response processing.
+    </d>
+    <r xlink:href="/rest/r/http_client"/>
+</p>

+ 9 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_allreleases.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<a xmlns="http://pear.php.net/dtd/rest.allreleases" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.allreleases http://pear.php.net/dtd/rest.allreleases.xsd">
+    <p>HTTP_Request</p>
+    <c>pear.net</c>
+    <r>
+        <v>1.4.0</v>
+        <s>stable</s>
+    </r>
+</a>

+ 1 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_deps.1.4.0.txt

@@ -0,0 +1 @@
+a:1:{s:8:"required";a:3:{s:3:"php";a:1:{s:3:"min";s:5:"4.0.0";}s:13:"pearinstaller";a:1:{s:3:"min";s:7:"1.4.0b1";}s:7:"package";a:2:{i:0;a:3:{s:4:"name";s:7:"Net_URL";s:7:"channel";s:12:"pear.dev.loc";s:3:"min";s:6:"1.0.12";}i:1;a:3:{s:4:"name";s:10:"Net_Socket";s:7:"channel";s:8:"pear.net";s:3:"min";s:5:"1.0.2";}}}}

+ 12 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_info.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<p xmlns="http://pear.php.net/dtd/rest.package" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.package http://pear.php.net/dtd/rest.package.xsd">
+    <n>HTTP_Request</n>
+    <c>pear.net</c>
+    <ca xlink:href="/rest/c/Default">Default</ca>
+    <l>BSD</l>
+    <s>Provides an easy way to perform HTTP requests</s>
+    <d>
+        Supports GET/POST/HEAD/TRACE/PUT/DELETE, Basic authentication, Proxy, Proxy Authentication, SSL, file uploads etc.
+    </d>
+    <r xlink:href="/rest/r/http_request"/>
+</p>

+ 6 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/packages.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<a xmlns="http://pear.php.net/dtd/rest.allpackages" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.allpackages http://pear.php.net/dtd/rest.allpackages.xsd">
+    <c>pear.net</c>
+    <p>HTTP_Client</p>
+    <p>HTTP_Request</p>
+</a>

+ 5 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.1/categories.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<a xmlns="http://pear.php.net/dtd/rest.allcategories" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.allcategories http://pear.php.net/dtd/rest.allcategories.xsd">
+    <ch>pear.net</ch>
+    <c xlink:href="/rest/c/Default/info.xml">Default</c>
+</a>

File diff suppressed because it is too large
+ 93 - 0
tests/Composer/Test/Repository/Pear/Fixtures/Rest1.1/packagesinfo.xml


+ 12 - 0
tests/Composer/Test/Repository/Pear/Fixtures/channel.1.0.xml

@@ -0,0 +1,12 @@
+<channel xmlns="http://pear.php.net/channel-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:schemaLocation="http://pear.php.net/channel-1.0 http://pear.php.net/dtd/channel-1.0.xsd">
+    <name>pear.net</name>
+    <summary>Test PEAR channel</summary>
+    <suggestedalias>test_alias</suggestedalias>
+    <servers>
+        <primary>
+            <rest>
+                <baseurl type="REST1.0">http://test.loc/rest10/</baseurl>
+            </rest>
+        </primary>
+    </servers>
+</channel>

+ 12 - 0
tests/Composer/Test/Repository/Pear/Fixtures/channel.1.1.xml

@@ -0,0 +1,12 @@
+<channel xmlns="http://pear.php.net/channel-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" xsi:schemaLocation="http://pear.php.net/channel-1.0 http://pear.php.net/dtd/channel-1.0.xsd">
+    <name>pear.net</name>
+    <summary>Test PEAR channel</summary>
+    <suggestedalias>test_alias</suggestedalias>
+    <servers>
+        <primary>
+            <rest>
+                <baseurl type="REST1.1">http://test.loc/rest11/</baseurl>
+            </rest>
+        </primary>
+    </servers>
+</channel>

+ 58 - 0
tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php

@@ -0,0 +1,58 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Repository\Pear;
+
+use Composer\Test\TestCase;
+
+class PackageDependencyParserTest extends TestCase
+{
+    /**
+     * @dataProvider dataProvider10
+     * @param $expected
+     * @param $data
+     */
+    public function testShouldParseDependencies($expected, $data10, $data20)
+    {
+        $expectedDependencies = array();
+        foreach ($expected as $expectedItem) {
+            $expectedDependencies[] = new DependencyConstraint(
+                $expectedItem['type'],
+                $expectedItem['constraint'],
+                $expectedItem['channel'],
+                $expectedItem['name']
+            );
+        }
+
+        $parser = new PackageDependencyParser();
+
+        if (false !== $data10) {
+            $result = $parser->buildDependencyInfo($data10);
+            $this->assertEquals($expectedDependencies, $result->getRequires() + $result->getOptionals(), "Failed for package.xml 1.0 format");
+        }
+
+        if (false !== $data20) {
+            $result = $parser->buildDependencyInfo($data20);
+            $this->assertEquals($expectedDependencies, $result->getRequires() + $result->getOptionals(), "Failed for package.xml 2.0 format");
+        }
+    }
+
+    public function dataProvider10()
+    {
+        $data = json_decode(file_get_contents(__DIR__.'/Fixtures/DependencyParserTestData.json'), true);
+        if (0 !== json_last_error()) {
+            throw new \PHPUnit_Framework_Exception('Invalid json file.');
+        }
+
+        return $data;
+    }
+}

+ 11 - 17
tests/Composer/Test/Repository/PearRepositoryTest.php

@@ -29,11 +29,11 @@ class PearRepositoryTest extends TestCase
      */
     private $remoteFilesystem;
 
-    public function testComposerNonCompatibleRepositoryShouldSetIncludePath()
+    public function testComposerShouldSetIncludePath()
     {
         $url = 'pear.phpmd.org';
         $expectedPackages = array(
-            array('name' => 'pear-phpmd/PHP_PMD', 'version' => '1.3.3'),
+            array('name' => 'pear-pear.phpmd.org/PHP_PMD', 'version' => '1.3.3'),
         );
 
         $repoConfig = array(
@@ -78,53 +78,47 @@ class PearRepositoryTest extends TestCase
     public function repositoryDataProvider()
     {
         return array(
-            array(
+           array(
                 'pear.phpunit.de',
                 array(
-                    array('name' => 'pear-phpunit/PHPUnit_MockObject', 'version' => '1.1.1'),
-                    array('name' => 'pear-phpunit/PHPUnit', 'version' => '3.6.10'),
+                    array('name' => 'pear-pear.phpunit.de/PHPUnit_MockObject', 'version' => '1.1.1'),
+                    array('name' => 'pear-pear.phpunit.de/PHPUnit', 'version' => '3.6.10'),
                 )
             ),
             array(
                 'pear.php.net',
                 array(
-                    array('name' => 'pear-pear/PEAR', 'version' => '1.9.4'),
+                    array('name' => 'pear-pear.php.net/PEAR', 'version' => '1.9.4'),
                 )
             ),
             array(
                 'pear.pdepend.org',
                 array(
-                    array('name' => 'pear-pdepend/PHP_Depend', 'version' => '1.0.5'),
+                    array('name' => 'pear-pear.pdepend.org/PHP_Depend', 'version' => '1.0.5'),
                 )
             ),
             array(
                 'pear.phpmd.org',
                 array(
-                    array('name' => 'pear-phpmd/PHP_PMD', 'version' => '1.3.3'),
+                    array('name' => 'pear-pear.phpmd.org/PHP_PMD', 'version' => '1.3.3'),
                 )
             ),
             array(
                 'pear.doctrine-project.org',
                 array(
-                    array('name' => 'pear-doctrine/DoctrineORM', 'version' => '2.2.2'),
+                    array('name' => 'pear-pear.doctrine-project.org/DoctrineORM', 'version' => '2.2.2'),
                 )
             ),
             array(
                 'pear.symfony-project.com',
                 array(
-                    array('name' => 'pear-symfony/YAML', 'version' => '1.0.6'),
+                    array('name' => 'pear-pear.symfony-project.com/YAML', 'version' => '1.0.6'),
                 )
             ),
             array(
                 'pear.pirum-project.org',
                 array(
-                    array('name' => 'pear-pirum/Pirum', 'version' => '1.1.4'),
-                )
-            ),
-            array(
-                'packages.zendframework.com',
-                array(
-                    array('name' => 'pear-zf2/Zend_Code', 'version' => '2.0.0.0-beta3'),
+                    array('name' => 'pear-pear.pirum-project.org/Pirum', 'version' => '1.1.4'),
                 )
             ),
         );

+ 2 - 0
tests/bootstrap.php

@@ -10,5 +10,7 @@
  * file that was distributed with this source code.
  */
 
+error_reporting(E_ALL);
+
 $loader = require __DIR__.'/../src/bootstrap.php';
 $loader->add('Composer\Test', __DIR__);

Some files were not shown because too many files changed in this diff