浏览代码

Add PEAR channel reader & Update PearRepository to use it.

Alexey Prilipko 13 年之前
父节点
当前提交
e173f11b37
共有 29 个文件被更改,包括 1951 次插入327 次删除
  1. 81 0
      src/Composer/Repository/Pear/BaseChannelReader.php
  2. 67 0
      src/Composer/Repository/Pear/ChannelInfo.php
  3. 200 0
      src/Composer/Repository/Pear/ChannelReader.php
  4. 164 0
      src/Composer/Repository/Pear/ChannelRest10Reader.php
  5. 136 0
      src/Composer/Repository/Pear/ChannelRest11Reader.php
  6. 60 0
      src/Composer/Repository/Pear/DependencyConstraint.php
  7. 50 0
      src/Composer/Repository/Pear/DependencyInfo.php
  8. 312 0
      src/Composer/Repository/Pear/PackageDependencyParser.php
  9. 94 0
      src/Composer/Repository/Pear/PackageInfo.php
  10. 50 0
      src/Composer/Repository/Pear/ReleaseInfo.php
  11. 101 308
      src/Composer/Repository/PearRepository.php
  12. 4 2
      tests/Composer/Test/Mock/RemoteFilesystemMock.php
  13. 144 0
      tests/Composer/Test/Repository/Pear/ChannelReaderTest.php
  14. 41 0
      tests/Composer/Test/Repository/Pear/ChannelRest10ReaderTest.php
  15. 37 0
      tests/Composer/Test/Repository/Pear/ChannelRest11ReaderTest.php
  16. 167 0
      tests/Composer/Test/Repository/Pear/Fixtures/DependencyParserTestData.json
  17. 9 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_allreleases.xml
  18. 1 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_deps.1.2.1.txt
  19. 14 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_client_info.xml
  20. 9 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_allreleases.xml
  21. 1 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_deps.1.4.0.txt
  22. 12 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/http_request_info.xml
  23. 6 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.0/packages.xml
  24. 5 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.1/categories.xml
  25. 93 0
      tests/Composer/Test/Repository/Pear/Fixtures/Rest1.1/packagesinfo.xml
  26. 12 0
      tests/Composer/Test/Repository/Pear/Fixtures/channel.1.0.xml
  27. 12 0
      tests/Composer/Test/Repository/Pear/Fixtures/channel.1.1.xml
  28. 58 0
      tests/Composer/Test/Repository/Pear/PackageDependencyParserTest.php
  29. 11 17
      tests/Composer/Test/Repository/PearRepositoryTest.php

+ 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 channelNS =               'http://pear.php.net/channel-1.0';
+    const allCategoriesNS =         'http://pear.php.net/dtd/rest.allcategories';
+    const categoryPackagesInfoNS =  'http://pear.php.net/dtd/rest.categorypackageinfo';
+    const allPackagesNS =           'http://pear.php.net/dtd/rest.allpackages';
+    const allReleasesNS =           'http://pear.php.net/dtd/rest.allreleases';
+    const packageInfoNS =           'http://pear.php.net/dtd/rest.package';
+
+    /** @var RemoteFilesystem */
+    private $rfs;
+
+    protected function __construct($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('The PEAR channel at '.$origin.' is broken.');
+        }
+
+        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;
+    }
+}

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

@@ -0,0 +1,200 @@
+<?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;
+use Composer\Package\LinkConstraint\VersionConstraint;
+use Composer\Package\Link;
+
+/**
+ * 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 channel supported REST interfaces and selects one of them
+     *
+     * @param $channelXml \SimpleXMLElement
+     * @param $supportedVersions string[] supported PEAR REST protocols
+     * @return array|bool hash with selected version and baseUrl
+     */
+    private function selectRestVersion($channelXml, $supportedVersions)
+    {
+        $channelXml->registerXPathNamespace('ns', self::channelNS);
+
+        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 false;
+    }
+
+    /**
+     * 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 (false === $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);
+    }
+
+    /**
+     * Builds MemoryPackages from PEAR package definition data.
+     *
+     * @param $channelName          string channel name
+     * @param $channelAlias         string channel alias
+     * @param $packageDefinitions   PackageInfo[]  package definition
+     * @return array
+     */
+    private function buildComposerPackages($channelName, $channelAlias, $packageDefinitions)
+    {
+        $versionParser = new \Composer\Package\Version\VersionParser();
+        $result = array();
+        foreach ($packageDefinitions as $packageDefinition) {
+            foreach ($packageDefinition->getReleases() as $version => $releaseInfo) {
+                $normalizedVersion = $this->parseVersion($version);
+                if (false === $normalizedVersion) {
+                    continue; // skip packages with unparsable versions
+                }
+
+                $composerPackageName = $this->buildComposerPackageName($packageDefinition->getChannelName(), $packageDefinition->getPackageName());
+
+                // distribution url must be read from /r/{packageName}/{version}.xml::/r/g:text()
+                $distUrl = "http://{$packageDefinition->getChannelName()}/get/{$packageDefinition->getPackageName()}-{$version}.tgz";
+
+                $requires = array();
+                $suggests = array();
+                $conflicts = array();
+                $replaces = array();
+
+                // alias package only when its channel matches repository channel,
+                // cause we've know only repository channel alias
+                if ($channelName == $packageDefinition->getChannelName()) {
+                    $composerPackageAlias = $this->buildComposerPackageName($channelAlias, $packageDefinition->getPackageName());
+                    $aliasConstraint = new VersionConstraint('==', $normalizedVersion);
+                    $aliasLink = new Link($composerPackageName, $composerPackageAlias, $aliasConstraint, 'replaces', (string) $aliasConstraint);
+                    $replaces[] = $aliasLink;
+                }
+
+                $dependencyInfo = $releaseInfo->getDependencyInfo();
+                foreach ($dependencyInfo->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;
+                    }
+                }
+
+                foreach ($dependencyInfo->getOptionals() as $groupName => $dependencyConstraints) {
+                    foreach ($dependencyConstraints as $dependencyConstraint) {
+                        $dependencyPackageName = $this->buildComposerPackageName($dependencyConstraint->getChannelName(), $dependencyConstraint->getPackageName());
+                        $suggests[$groupName.'-'.$dependencyPackageName] = $dependencyConstraint->getConstraint();
+                    }
+                }
+
+                $package = new \Composer\Package\MemoryPackage($composerPackageName, $normalizedVersion, $version);
+                $package->setType('library');
+                $package->setDescription($packageDefinition->getDescription());
+                $package->setDistType('pear');
+                $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;
+            }
+        }
+
+        return $result;
+    }
+
+    private function buildComposerPackageName($pearChannelName, $pearPackageName)
+    {
+        if ($pearChannelName == 'php') {
+            return "php";
+        }
+        if ($pearChannelName == 'ext') {
+            return "ext-{$pearPackageName}";
+        }
+
+        return "pear-{$pearChannelName}/{$pearPackageName}";
+    }
+
+    protected 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;
+        } else {
+            return false;
+        }
+    }
+}

+ 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::allPackagesNS);
+        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::packageInfoNS);
+
+        $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::allReleasesNS);
+            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::allCategoriesNS);
+        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::categoryPackagesInfoNS);
+        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::categoryPackagesInfoNS);
+        $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;
+    }
+}

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

@@ -0,0 +1,312 @@
+<?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 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;
+    }
+
+    /**
+     * 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());
+        } elseif (!$this->isHash($depArray)) {
+            return new DependencyInfo($this->buildDependency10Info($depArray), array());
+        } else {
+            return $this->buildDependency20Info($depArray);
+        }
+    }
+
+    /**
+     * 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 '*';
+        } elseif (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 bool|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;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 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;
+    }
+}

+ 101 - 308
src/Composer/Repository/PearRepository.php

@@ -13,26 +13,33 @@
 namespace Composer\Repository;
 
 use Composer\IO\IOInterface;
-use Composer\Package\Loader\ArrayLoader;
+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;
 
+    /** @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)
     {
         if (!preg_match('{^https?://}', $repoConfig['url'])) {
@@ -44,9 +51,9 @@ 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;
     }
 
     protected function initialize()
@@ -54,341 +61,127 @@ 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 \Composer\Repository\Pear\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->io->write('<warning>PEAR repository from '.$this->url.' could not be loaded. '.$e->getMessage().'</warning>');
+            return;
         }
-
-        $this->fetchFromServer();
-    }
-
-    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';
-        }
-
-        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);
+        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)
     {
-        $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;
+        $versionParser = new \Composer\Package\Version\VersionParser();
+        $result = array();
+        foreach ($channelInfo->getPackages() as $packageDefinition) {
+            foreach ($packageDefinition->getReleases() as $version => $releaseInfo) {
+                $normalizedVersion = $this->parseVersion($version);
+                if (false === $normalizedVersion) {
+                    continue; // skip packages with unparsable versions
                 }
-                throw $e;
-            }
 
-            $releases = $releasesXML->getElementsByTagName('r');
+                $composerPackageName = $this->buildComposerPackageName($packageDefinition->getChannelName(), $packageDefinition->getPackageName());
 
-            foreach ($releases as $release) {
-                /* @var $release \DOMElement */
-                $pearVersion = $release->getElementsByTagName('v')->item(0)->nodeValue;
+                // 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";
 
-                $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('/'),
-                );
+                $requires = array();
+                $suggests = array();
+                $conflicts = array();
+                $replaces = array();
 
-                try {
-                    $deps = $this->rfs->getContents($this->url, $releaseLink . "/deps.".$pearVersion.".txt", false);
-                } catch (TransportException $e) {
-                    if (strpos($e->getMessage(), '404')) {
-                        continue;
-                    }
-                    throw $e;
+                // 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);
                 }
 
-                $packageData += $this->parseDependencies($deps);
-
-                try {
-                    $this->addPackage($loader->load($packageData));
-                    if ($this->io->isVerbose()) {
-                        $this->io->write('Loaded '.$packageData['name'].' '.$packageData['version']);
-                    }
-                } catch (\UnexpectedValueException $e) {
-                    if ($this->io->isVerbose()) {
-                        $this->io->write('Could not load '.$packageData['name'].' '.$packageData['version'].': '.$e->getMessage());
-                    }
-                    continue;
+                // 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);
                 }
-            }
-        }
-    }
 
-    /**
-     * @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'];
-        }
-
-        return implode(',', $versions);
-    }
-
-    /**
-     * @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;
+                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;
                     }
+                }
 
-                    if (is_array($value)) {
-                        $dataKey = $value['name'];
-                        if (false === strpos($dataKey, '/')) {
-                            $dataKey = $this->getChannelShorthand($value['channel']).'/'.$dataKey;
-                        }
-                        $data['pear-'.$dataKey] = $this->parseVersion($value);
+                foreach ($releaseInfo->getDependencyInfo()->getOptionals() as $group => $dependencyConstraints) {
+                    foreach ($dependencyConstraints as $dependencyConstraint) {
+                        $dependencyPackageName = $this->buildComposerPackageName($dependencyConstraint->getChannelName(), $dependencyConstraint->getPackageName());
+                        $suggests[$group.'-'.$dependencyPackageName] = $dependencyConstraint->getConstraint();
                     }
                 }
-            } elseif ('extension' == $name) {
-                foreach ($options as $key => $value) {
-                    $dataKey = 'ext-' . $value['name'];
-                    $data[$dataKey] = $this->parseVersion($value);
-                }
-            }
-        }
 
-        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.");
+                $package = new MemoryPackage($composerPackageName, $normalizedVersion, $version);
+                $package->setType('library');
+                $package->setDescription($packageDefinition->getDescription());
+                $package->setDistType('pear');
+                $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;
             }
         }
-        $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;
+        return $result;
     }
 
-    /**
-     * @param  string                    $packagesLink
-     * @return void
-     * @throws \InvalidArgumentException
-     */
-    private function fetchPear2Packages($packagesLink)
+    private function buildComposerPackageName($pearChannelName, $pearPackageName)
     {
-        $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;
-                }
-            }
-
-            $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];
-                }
-
-                $package = $packageData + $releaseData;
-                try {
-                    $this->addPackage($loader->load($package));
-                    if ($this->io->isVerbose()) {
-                        $this->io->write('Loaded '.$package['name'].' '.$package['version']);
-                    }
-                } catch (\UnexpectedValueException $e) {
-                    if ($this->io->isVerbose()) {
-                        $this->io->write('Could not load '.$package['name'].' '.$package['version'].': '.$e->getMessage());
-                    }
-                    continue;
-                }
-            }
+        if ($pearChannelName == 'php') {
+            return "php";
+        } elseif ($pearChannelName == 'ext') {
+            return "ext-{$pearPackageName}";
+        } else {
+            return "pear-{$pearChannelName}/{$pearPackageName}";
         }
     }
 
-    /**
-     * @param  string       $url
-     * @return \DOMDocument
-     */
-    private function requestXml($url)
+    protected function parseVersion($version)
     {
-        $content = $this->rfs->getContents($this->url, $url, false);
-        if (!$content) {
-            throw new \UnexpectedValueException('The PEAR channel at '.$url.' did not respond.');
-        }
-        $dom = new \DOMDocument('1.0', 'UTF-8');
-        $dom->loadXML($content);
+        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 $dom;
-    }
-
-    private function getChannelShorthand($url)
-    {
-        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, '.'));
-            }
+            return $version;
+        } else {
+            return false;
         }
-
-        return self::$channelNames[$url];
     }
 }

+ 4 - 2
tests/Composer/Test/Mock/RemoteFilesystemMock.php

@@ -13,6 +13,7 @@
 namespace Composer\Test\Mock;
 
 use Composer\Util\RemoteFilesystem;
+use Composer\Downloader\TransportException;
 
 /**
  * Remote filesystem mock
@@ -29,10 +30,11 @@ class RemoteFilesystemMock extends RemoteFilesystem
 
     public function getContents($originUrl, $fileUrl, $progress = true)
     {
-        if(!empty($this->contentMap[$fileUrl]))
+        if (!empty($this->contentMap[$fileUrl])) {
             return $this->contentMap[$fileUrl];
+        }
 
-        throw new \Composer\Downloader\TransportException('The "'.$fileUrl.'" file could not be downloaded (NOT FOUND)', 404);
+        throw new TransportException('The "'.$fileUrl.'" file could not be downloaded (NOT FOUND)', 404);
     }
 
 }

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

@@ -0,0 +1,144 @@
+<?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\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\Pear\ChannelReader')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $ref = new \ReflectionMethod($reader, 'buildComposerPackages');
+        $ref->setAccessible(true);
+
+        $packageInfo = 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, 'test.loc', 'test', array($packageInfo));
+
+        $expectedPackage = new MemoryPackage('pear-test.loc/sample', '1.0.0.1' , '1.0.0.1');
+        $expectedPackage->setType('library');
+        $expectedPackage->setDistType('pear');
+        $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>

文件差异内容过多而无法显示
+ 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'),
                 )
             ),
         );

部分文件因为文件数量过多而无法显示