File: //usr/share/php/pkgtools/phpcomposer/source.php
<?php
/*
* Copyright (c) 2014 Mathieu Parent <[email protected]>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
namespace Pkgtools\Phpcomposer;
use \Pkgtools\Base\Logger;
/**
* This class parses composer.json
*
* @copyright Copyright (c) 2014 Mathieu Parent <[email protected]>
* @author Mathieu Parent <[email protected]>
* @license Expat http://www.jclark.com/xml/copying.txt
*/
class Source {
/**
* composer.json file path
*
* @var string
*/
protected $_path = NULL;
/**
* Decoded composer.json
*
* @var mixed
*/
protected $_json = NULL;
/**
* Constructor
*
* @param string $filename
*/
function __construct($dir_name) {
// Find composer.json
$dir_name = realpath($dir_name);
if (is_file("$dir_name/composer.json")) {
$this->_path = "$dir_name/composer.json";
}
if (is_null($this->_path)) {
throw new \InvalidArgumentException('composer.json not found');
}
// Load file
$data = file_get_contents($this->_path);
if ($data === false) {
throw new \InvalidArgumentException("Unable to open composer.json ($this->_path)");
}
// Parse JSON
if (!function_exists('json_decode')) {
throw new \InvalidArgumentException('JSON extension is not installed or not loaded');
}
$this->_json = json_decode($data, true);
if ($this->_json === NULL) {
switch (json_last_error()) {
case JSON_ERROR_NONE:
$json_error = 'No errors';
break;
case JSON_ERROR_DEPTH:
$json_error = 'Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$json_error = 'Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$json_error = 'Unexpected control character found';
break;
case JSON_ERROR_SYNTAX:
$json_error = 'Syntax error, malformed JSON';
break;
case JSON_ERROR_UTF8:
$json_error = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$json_error = 'Unknown error';
break;
}
if (function_exists('json_last_error_msg')) {
$json_error_msg = json_last_error_msg();
} else {
$json_error_msg = '';
}
throw new \InvalidArgumentException("Error parsing composer.json: $json_error ($json_error_msg)");
}
}
/**
* Raw properties getter
*/
function __get($property) {
switch($property) {
case 'name':
case 'description':
return $this->_json[$property];
default:
throw new \InvalidArgumentException("Unknown property: '$property'");
}
}
/**
* Does the package has a file with role "script"
*/
function hasPhpScript() {
return !empty($this->_json['bin']);
}
/**
* Dependencies
*/
function getDependencies() {
$result = new \Pkgtools\Base\Dependencies();
$levels = Array(
'require',
'require-dev',
'recommend',
'suggest',
'conflict',
'provide',
'replace',
);
if ($this->hasPhpScript()) {
$dep = new \Pkgtools\Base\Dependency('require', '', 'php-cli');
$result[] = $dep;
} else {
$dep = new \Pkgtools\Base\Dependency('require', '', 'php');
$result[] = $dep;
}
foreach ($levels as $level) {
if (!empty($this->_json[$level])) {
foreach($this->_json[$level] as $project_package => $versions) {
Logger::debug('Parsing dependency %s:%s (%s) from file "%s".', $level, $project_package, $versions, $this->_path);
if (strpos($project_package, '/') !== FALSE) {
list($project, $package) = explode('/', $project_package, 2);
} else {
$project = '';
$package = $project_package;
}
$dep = new \Pkgtools\Base\Dependency($level, $project, $package);
if (strpos($versions, '|') !== FALSE) {
Logger::warning('OR-ed versions are not supported %s:%s (%s) in file "%s".', $level, $project_package, $versions, $this->_path);
} else {
try {
$operator_regexp = '(==?|!=|<>|>=?|<=?|~|\^)'; // $1
$versions = preg_replace("/([^,])\s+$operator_regexp/", '\1,\2', $versions);
foreach(explode(',', $versions) as $version) {
// Construct regexp
$version_regexp = 'v?([0-9.*]*|self\.version|dev-\w+)'; // $2
$stability_regexp = '(-dev|-patch\d*|-alpha\d*|-beta\d*|-RC\d*)?'; // $3
$stabilityflag_regexp = '((?i)@dev|@alpha|@beta|@RC|@stable)?'; // $4
$inlinealias_regexp = '(?:\s+as\s+(\S+))?'; // $5
if (preg_match("/^\s*$operator_regexp?\s*$version_regexp$stability_regexp\s*$stabilityflag_regexp$inlinealias_regexp\s*$/", $version, $operator_matches)) {
$operator = $operator_matches[1];
$base_version = $operator_matches[2];
if (!empty($operator_matches[3])) {
switch($operator_matches[3][1]) {
case 'd': // dev
case 'p': // patch
$version = $base_version . '~~' . substr($operator_matches[3], 1);
break;
default:
$version = $base_version . '~' . substr($operator_matches[3], 1);
}
} else {
$version = $base_version;
}
if (substr($version, 0, 4) == 'dev-') {
Logger::info('Branch alias mapped to "*" %s:%s (%s) in file "%s".', $level, $project_package, $versions, $this->_path);
$version = '*';
}
} else {
throw new \InvalidArgumentException("Unable to parse version '$version' with dependency $project_package ($versions)");
}
if (($operator == '') || ($operator == '=') || ($operator == '==')) {
if (($version == '*') || ($version == '')) {
// no version constraints
} elseif (substr($version, -1) == '*') {
// x.y.* -> (>= x.y), (<< x.y+1~~)
$version_components = explode('.', $version);
array_pop($version_components); // Pop '*'
$last_version_component = array_pop($version_components);
$dep->minVersion = implode('.', array_merge($version_components, Array($last_version_component)));
$dep->maxVersion = implode('.', array_merge($version_components, Array($last_version_component + 1))) . '~~';
} else {
$dep->minVersion = $version;
$dep->maxVersion = $version;
$dep->excludeMaxVersion = false;
}
} elseif (($operator == '!=') || ($operator == '<>')) {
// We turn this into a conflict
$dep2 = clone($dep);
$dep2->level = 'conflict';
$dep->minVersion = $version;
$dep->maxVersion = $version;
$dep->excludeMaxVersion = false;
$result[] = $dep2;
} elseif (($operator == '>') || ($operator == '>=')) {
$dep->minVersion = $version;
$dep->excludeMinVersion = $operator == '>';
} elseif ($operator == '<') {
$dep->maxVersion = $base_version.'~~';
$dep->excludeMaxVersion = true;
} elseif ($operator == '<=') {
$dep->maxVersion = $version;
$dep->excludeMaxVersion = false;
} elseif ($operator == '~') {
// ~x.y.z -> (>= x.y.z), (<< x.y+1~~)
$version_components = explode('.', $version);
if (count($version_components) > 1) {
array_pop($version_components);
$last_version_component = array_pop($version_components);
} else {
$last_version_component = array_pop($version_components);
}
$dep->minVersion = $version;
$dep->maxVersion = implode('.', array_merge($version_components, Array($last_version_component + 1))) . '~~';
} elseif ($operator == '^') {
// ^x.y.z -> (>= x.y.z), (<< x+1~~) if x >= 1
// ^x.y.z -> (>= x.y.z), (<< x.y+1~~) if x == 0
$version_components = explode('.', $version);
$prefix_components = Array();
$significant_version_component = array_shift($version_components);
if ($significant_version_component == '0') {
array_unshift($prefix_components, $significant_version_component);
$significant_version_component = array_shift($version_components);
}
$dep->minVersion = $version;
$dep->maxVersion = implode('.', array_merge(
$prefix_components,
Array($significant_version_component + 1))). '~~';
} else {
throw new \InvalidArgumentException("Unable to parse version operator '$operator' with dependency $project_package ($versions)");
}
}
} catch(\Exception $e) {
// suggest can have free text in place of version constraints
if ($dep->level != 'suggest') {
throw new \Exception($e->getMessage(). " with dependency $project_package ($versions)", $e->getCode(), $e);
}
}
}
$result[] = $dep;
}
}
}
return $result;
}
}