From 6929c42848ea843a0652e93bcad0ddbf193f4f9f Mon Sep 17 00:00:00 2001 From: digitalkaoz Date: Fri, 9 Dec 2011 12:16:17 +0100 Subject: [PATCH] added schema/syntax validation for composer.json --- bin/installer | 151 ---------------------- composer.json | 4 +- res/composer-schema.json | 3 +- src/Composer/Command/ValidateCommand.php | 2 +- src/Composer/Console/Application.php | 2 +- src/Composer/Factory.php | 2 +- src/Composer/Json/JsonFile.php | 105 ++++++++------- tests/Composer/Test/Json/JsonFileTest.php | 34 +++-- 8 files changed, 86 insertions(+), 217 deletions(-) delete mode 100755 bin/installer diff --git a/bin/installer b/bin/installer deleted file mode 100755 index 798757f64..000000000 --- a/bin/installer +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env php - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - - -process($argv); - -/** - * processes the installer - */ -function process($argv) -{ - $check = in_array('--check', $argv); - $help = in_array('--help', $argv); - $force = in_array('--force', $argv); - - if ($help) { - displayHelp(); - exit(0); - } - - $ok = checkPlatform(); - - if ($check && !$ok) { - exit(1); - } - - if ($ok || $force) { - installComposer(); - } - - exit(0); -} - -/** - * displays the help - */ -function displayHelp() -{ - echo << $actual) { - switch ($error) { - case 'unicode': - $text = " detect_unicode = Off (actual: {$actual})".PHP_EOL; - break; - - case 'readonly': - $text = " phar.readonly = Off (actual: {$actual})".PHP_EOL; - break; - - case 'require_hash': - $text = " phar.require_hash = Off (actual: {$actual})".PHP_EOL; - break; - - case 'suhosin': - $text = " suhosin.executor.include.whitelist = phar (actual: {$actual})".PHP_EOL; - break; - case 'php': - $text = " PHP_VERSION (actual: {$actual})".PHP_EOL; - break; - } - out($text, 'info'); - } - echo PHP_EOL; - return false; - } - - out("All settings correct for using Composer".PHP_EOL,'success'); - return true; -} - -/** - * installs composer to the current working directory - */ -function installComposer() -{ - $installDir = getcwd(); - $file = $installDir . DIRECTORY_SEPARATOR . 'composer.phar'; - - if (is_readable($file)) { - @unlink($file); - } - - $download = copy('http://getcomposer.org/composer.phar', $installDir.DIRECTORY_SEPARATOR.'composer.phar'); - - out(PHP_EOL."Composer successfully installed to: " . $file, 'success'); - out(PHP_EOL."Use it: php composer.phar".PHP_EOL, 'info'); -} - -/** - * colorize output - */ -function out($text, $color = null) -{ - $styles = array( - 'success' => "\033[0;32m%s\033[0m", - 'error' => "\033[31;31m%s\033[0m", - 'info' => "\033[33;33m%s\033[0m" - ); - - echo sprintf(isset($styles[$color]) ? $styles[$color] : "%s", $text); -} \ No newline at end of file diff --git a/composer.json b/composer.json index 7b89586b2..236584f85 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,9 @@ } ], "require": { - "php": ">=5.3.0", + "php": ">=5.3.0", + "justinrainbow/json-schema": ">=1.1.0", + "seld/jsonlint": "*", "symfony/console": "dev-master", "symfony/finder": "dev-master", "symfony/process": "dev-master" diff --git a/res/composer-schema.json b/res/composer-schema.json index 62d6be0ed..ed25a5b07 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -35,8 +35,7 @@ }, "version": { "type": "string", - "description": "Package version, see http://packagist.org/about for more info on valid schemes.", - "required": true + "description": "Package version, see http://packagist.org/about for more info on valid schemes." }, "time": { "type": "string", diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index d862a2b61..41f516ad5 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -53,7 +53,7 @@ EOT } try { - JsonFile::parseJson(file_get_contents($file)); + JsonFile::parseJson(file_get_contents($file), true); } catch (\Exception $e) { $output->writeln(''.$e->getMessage().''); return 1; diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index fe6b6cb13..1881fb575 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -128,4 +128,4 @@ class Application extends BaseApplication return $helperSet; } -} +} \ No newline at end of file diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index e27976a4b..f01d39b1a 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -54,7 +54,7 @@ class Factory 'vendor-dir' => 'vendor', ); - $packageConfig = $file->read(); + $packageConfig = $file->read(true); if (isset($packageConfig['config']) && is_array($packageConfig['config'])) { $packageConfig['config'] = array_merge($composerConfig, $packageConfig['config']); diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 0fc0105b3..110401688 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -14,6 +14,8 @@ namespace Composer\Json; use Composer\Repository\RepositoryManager; use Composer\Composer; +use JsonSchema\Validator; +use Seld\JsonLint\JsonParser; use Composer\Util\StreamContextFactory; if (!defined('JSON_UNESCAPED_SLASHES')) { @@ -68,7 +70,7 @@ class JsonFile * * @return array */ - public function read() + public function read($validate = false) { $ctx = StreamContextFactory::getContext(array( 'http' => array( @@ -80,7 +82,7 @@ class JsonFile throw new \RuntimeException('Could not read '.$this->path.', you are probably offline'); } - return static::parseJson($json); + return static::parseJson($json, $validate); } /** @@ -215,59 +217,64 @@ class JsonFile /** * Parses json string and returns hash. * - * @param string $json json string + * @param string $json json string + * @param boolean $validateSchema wether to validate the json schema * * @return mixed */ - static public function parseJson($json) - { - $data = json_decode($json, true); + public static function parseJson($json, $validateSchema=false) + { + $data = static::validateSyntax($json); - if (null === $data && 'null' !== strtolower($json)) { - switch (json_last_error()) { - case JSON_ERROR_NONE: - $msg = 'No error has occurred, is your composer.json file empty?'; - break; - case JSON_ERROR_DEPTH: - $msg = 'The maximum stack depth has been exceeded'; - break; - case JSON_ERROR_STATE_MISMATCH: - $msg = 'Invalid or malformed JSON'; - break; - case JSON_ERROR_CTRL_CHAR: - $msg = 'Control character error, possibly incorrectly encoded'; - break; - case JSON_ERROR_SYNTAX: - $msg = 'Syntax error'; - $charOffset = 0; - if (preg_match('#["}\]]\s*(,)\s*[}\]]#', $json, $match, PREG_OFFSET_CAPTURE)) { - $msg .= ', extra comma'; - } elseif (preg_match('#((?<=[^\\\\])\\\\(?!["\\\\/bfnrt]|u[a-f0-9]{4}))#i', $json, $match, PREG_OFFSET_CAPTURE)) { - $msg .= ', unescaped backslash (\\)'; - } elseif (preg_match('#(["}\]])(?: *\r?\n *)+"#', $json, $match, PREG_OFFSET_CAPTURE)) { - $msg .= ', missing comma'; - $charOffset = 1; - } elseif (preg_match('#^ *([a-z0-9_-]+) *:#mi', $json, $match, PREG_OFFSET_CAPTURE)) { - $msg .= ', you must use double quotes (") around keys'; - } elseif (preg_match('#(\'.+?\' *:|: *\'.+?\')#', $json, $match, PREG_OFFSET_CAPTURE)) { - $msg .= ', use double quotes (") instead of single quotes (\')'; - } elseif (preg_match('#(\[".*?":.*?\])#', $json, $match, PREG_OFFSET_CAPTURE)) { - $msg .= ', you must use the hash syntax (e.g. {"foo": "bar"}) instead of array syntax (e.g. ["foo", "bar"])'; - } elseif (preg_match('#".*?"( *["{\[])#', $json, $match, PREG_OFFSET_CAPTURE)) { - $msg .= ', missing colon'; - } - if (isset($match[1][1])) { - $preError = substr($json, 0, $match[1][1]); - $msg .= ' on line '.(substr_count($preError, "\n")+1).', char '.abs(strrpos($preError, "\n") - strlen($preError) - $charOffset); - } - break; - case JSON_ERROR_UTF8: - $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; - break; - } - throw new \UnexpectedValueException('JSON Parse Error: '.$msg); + if ($validateSchema) { + static::validateSchema($json); } return $data; } + + /** + * validates a composer.json against the schema + * + * @param string $json + * @return boolean + * @throws \UnexpectedValueException + */ + public static function validateSchema($json) + { + $data = json_decode($json); + $schema = json_decode(file_get_contents(__DIR__ . '/../../../res/composer-schema.json')); + + $validator = new Validator(); + + $validator->check($data, $schema); + + if (!$validator->isValid()) { + $msg = "\n"; + foreach ((array) $validator->getErrors() as $error) { + $msg .= ($error['property'] ? $error['property'].' : ' : '').$error['message']."\n"; + } + + throw new \UnexpectedValueException('Your composer.json did not validate against the schema. The following mistakes were found:'.$msg); + } + } + + /** + * validates the json syntax + * + * @param string $json + * @return array + * @throws \UnexpectedValueException + */ + public static function validateSyntax($json) + { + $parser = new JsonParser(); + $result = $parser->lint($json); + + if (null === $result) { + return json_decode($json, true); + } + + throw $result; + } } diff --git a/tests/Composer/Test/Json/JsonFileTest.php b/tests/Composer/Test/Json/JsonFileTest.php index fc205eceb..7bb7aecc5 100644 --- a/tests/Composer/Test/Json/JsonFileTest.php +++ b/tests/Composer/Test/Json/JsonFileTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Json; +use Seld\JsonLint\ParsingException; use Composer\Json\JsonFile; class JsonFileTest extends \PHPUnit_Framework_TestCase @@ -21,7 +22,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase $json = '{ "foo": "bar", }'; - $this->expectParseException('extra comma on line 2, char 21', $json); + $this->expectParseException('Parse error on line 2', $json); } public function testParseErrorDetectExtraCommaInArray() @@ -31,7 +32,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase "bar", ] }'; - $this->expectParseException('extra comma on line 3, char 18', $json); + $this->expectParseException('Parse error on line 3', $json); } public function testParseErrorDetectUnescapedBackslash() @@ -39,7 +40,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase $json = '{ "fo\o": "bar" }'; - $this->expectParseException('unescaped backslash (\\) on line 2, char 12', $json); + $this->expectParseException('Parse error on line 1', $json); } public function testParseErrorSkipsEscapedBackslash() @@ -48,7 +49,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase "fo\\\\o": "bar" "a": "b" }'; - $this->expectParseException('missing comma on line 2, char 23', $json); + $this->expectParseException('Parse error on line 2', $json); } public function testParseErrorDetectSingleQuotes() @@ -56,7 +57,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase $json = '{ \'foo\': "bar" }'; - $this->expectParseException('use double quotes (") instead of single quotes (\') on line 2, char 9', $json); + $this->expectParseException('Parse error on line 1', $json); } public function testParseErrorDetectMissingQuotes() @@ -64,7 +65,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase $json = '{ foo: "bar" }'; - $this->expectParseException('must use double quotes (") around keys on line 2, char 9', $json); + $this->expectParseException('Parse error on line 1', $json); } public function testParseErrorDetectArrayAsHash() @@ -72,7 +73,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase $json = '{ "foo": ["bar": "baz"] }'; - $this->expectParseException('you must use the hash syntax (e.g. {"foo": "bar"}) instead of array syntax (e.g. ["foo", "bar"]) on line 2, char 16', $json); + $this->expectParseException('Parse error on line 2', $json); } public function testParseErrorDetectMissingComma() @@ -81,7 +82,18 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase "foo": "bar" "bar": "foo" }'; - $this->expectParseException('missing comma on line 2, char 21', $json); + $this->expectParseException('Parse error on line 2', $json); + } + + public function testSchemaValidation() + { + $json = file_get_contents(__DIR__.'/../../../../composer.json'); + + try { + $this->assertNull(JsonFile::validateSchema($json)); + } catch (\UnexpectedValueException $e) { + $this->fail('invalid schema'); + } } public function testParseErrorDetectMissingCommaMultiline() @@ -91,7 +103,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase "bar": "foo" }'; - $this->expectParseException('missing comma on line 2, char 24', $json); + $this->expectParseException('Parse error on line 2', $json); } public function testParseErrorDetectMissingColon() @@ -100,7 +112,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase "foo": "bar", "bar" "foo" }'; - $this->expectParseException('missing colon on line 3, char 14', $json); + $this->expectParseException('Parse error on line 3', $json); } public function testSimpleJsonString() @@ -166,7 +178,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase try { JsonFile::parseJson($json); $this->fail(); - } catch (\UnexpectedValueException $e) { + } catch (ParsingException $e) { $this->assertContains($text, $e->getMessage()); } }