diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php index 604384c97..50234d775 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -17,7 +17,9 @@ namespace Composer\Json; */ class JsonManipulator { - private static $RECURSE_BLOCKS = '(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\})*\})*'; + private static $RECURSE_BLOCKS; + private static $JSON_VALUE; + private static $JSON_STRING; private $contents; private $newline; @@ -25,6 +27,12 @@ class JsonManipulator public function __construct($contents) { + if (!self::$RECURSE_BLOCKS) { + self::$RECURSE_BLOCKS = '(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\})*\})*'; + self::$JSON_STRING = '"(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x09\x0a-\x1f\\\\"])*"'; + self::$JSON_VALUE = '(?:[0-9.]+|null|true|false|'.self::$JSON_STRING.'|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})'; + } + $contents = trim($contents); if (!preg_match('#^\{(.*)\}$#s', $contents)) { throw new \InvalidArgumentException('The json file must be an object ({})'); @@ -43,7 +51,7 @@ class JsonManipulator { // no link of that type yet if (!preg_match('#"'.$type.'":\s*\{#', $this->contents)) { - $this->addMainKey($type, $this->format(array($package => $constraint))); + $this->addMainKey($type, array($package => $constraint)); return true; } @@ -100,7 +108,7 @@ class JsonManipulator { // no main node yet if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) { - $this->addMainKey(''.$mainNode.'', $this->format(array($name => $value))); + $this->addMainKey(''.$mainNode.'', array($name => $value)); return true; } @@ -126,8 +134,8 @@ class JsonManipulator $that = $this; // child exists - if (preg_match('{("'.preg_quote($name).'"\s*:\s*)([0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})(,?)}', $children, $matches)) { - $children = preg_replace_callback('{("'.preg_quote($name).'"\s*:\s*)([0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})(,?)}', function ($matches) use ($name, $subName, $value, $that) { + if (preg_match('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', $children, $matches)) { + $children = preg_replace_callback('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', function ($matches) use ($name, $subName, $value, $that) { if ($subName !== null) { $curVal = json_decode($matches[2], true); $curVal[$subName] = $value; @@ -194,7 +202,7 @@ class JsonManipulator // try and find a match for the subkey if (preg_match('{"'.preg_quote($name).'"\s*:}i', $children)) { // find best match for the value of "name" - if (preg_match_all('{"'.preg_quote($name).'"\s*:\s*(?:[0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})}', $children, $matches)) { + if (preg_match_all('{"'.preg_quote($name).'"\s*:\s*(?:'.self::$JSON_VALUE.')}', $children, $matches)) { $bestMatch = ''; foreach ($matches[0] as $match) { if (strlen($bestMatch) < strlen($match)) { @@ -241,19 +249,41 @@ class JsonManipulator public function addMainKey($key, $content) { + $content = $this->format($content); + + // key exists already + $regex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'. + '('.preg_quote(JsonFile::encode($key)).'\s*:\s*'.self::$JSON_VALUE.')(.*)}s'; + if (preg_match($regex, $this->contents, $matches)) { + // invalid match due to un-regexable content, abort + if (!json_decode('{'.$matches[2].'}')) { + return false; + } + + $this->contents = $matches[1] . JsonFile::encode($key).': '.$content . $matches[3]; + + return true; + } + + // append at the end of the file and keep whitespace if (preg_match('#[^{\s](\s*)\}$#', $this->contents, $match)) { $this->contents = preg_replace( '#'.$match[1].'\}$#', addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\'), $this->contents ); - } else { - $this->contents = preg_replace( - '#\}$#', - addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\'), - $this->contents - ); + + return true; } + + // append at the end of the file + $this->contents = preg_replace( + '#\}$#', + addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\'), + $this->contents + ); + + return true; } public function format($data, $depth = 0) diff --git a/tests/Composer/Test/Json/JsonManipulatorTest.php b/tests/Composer/Test/Json/JsonManipulatorTest.php index 6f23e92f1..8fa105294 100644 --- a/tests/Composer/Test/Json/JsonManipulatorTest.php +++ b/tests/Composer/Test/Json/JsonManipulatorTest.php @@ -611,6 +611,57 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase } } } +', $manipulator->getContents()); + } + + public function testAddMainKey() + { + $manipulator = new JsonManipulator('{ + "foo": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('bar', 'baz')); + $this->assertEquals('{ + "foo": "bar", + "bar": "baz" +} +', $manipulator->getContents()); + } + + public function testUpdateMainKey() + { + $manipulator = new JsonManipulator('{ + "foo": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('foo', 'baz')); + $this->assertEquals('{ + "foo": "baz" +} +', $manipulator->getContents()); + } + + public function testUpdateMainKey2() + { + $manipulator = new JsonManipulator('{ + "a": { + "foo": "bar", + "baz": "qux" + }, + "foo": "bar", + "baz": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('foo', 'baz')); + $this->assertTrue($manipulator->addMainKey('baz', 'quux')); + $this->assertEquals('{ + "a": { + "foo": "bar", + "baz": "qux" + }, + "foo": "baz", + "baz": "quux" +} ', $manipulator->getContents()); } }