new stdClass, 'emptyArray' => array(), 'string' => 'foobar\\', 'filledArray' => array( array( 123, 456, ), // Nested json works without problems '"7":["8",{"9":"10"}]', // Whitespace clean up doesn't touch strings that look alike "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}", ), ); // No trailing whitespace, no trailing linefeed $json = '{ "emptyObject": {}, "emptyArray": [], "string": "foobar\\\\", "filledArray": [ [ 123, 456 ], "\"7\":[\"8\",{\"9\":\"10\"}]", "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}" ] }'; $json = str_replace( "\r", '', $json ); // Windows compat $json = str_replace( "\t", $expectedIndent, $json ); $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) ); } public static function provideEncodeDefault() { return self::getEncodeTestCases( array() ); } /** * @dataProvider provideEncodeDefault */ public function testEncodeDefault( $from, $to ) { $this->assertSame( $to, FormatJson::encode( $from ) ); } public static function provideEncodeUtf8() { return self::getEncodeTestCases( array( 'unicode' ) ); } /** * @dataProvider provideEncodeUtf8 */ public function testEncodeUtf8( $from, $to ) { $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) ); } public static function provideEncodeXmlMeta() { return self::getEncodeTestCases( array( 'xmlmeta' ) ); } /** * @dataProvider provideEncodeXmlMeta */ public function testEncodeXmlMeta( $from, $to ) { $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) ); } public static function provideEncodeAllOk() { return self::getEncodeTestCases( array( 'unicode', 'xmlmeta' ) ); } /** * @dataProvider provideEncodeAllOk */ public function testEncodeAllOk( $from, $to ) { $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) ); } public function testEncodePhpBug46944() { $this->assertNotEquals( '\ud840\udc00', strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ), 'Test encoding an broken json_encode character (U+20000)' ); } public function testDecodeReturnType() { $this->assertInternalType( 'object', FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ), 'Default to object' ); $this->assertInternalType( 'array', FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ), 'Optional array' ); } public static function provideParse() { return array( array( null ), array( true ), array( false ), array( 0 ), array( 1 ), array( 1.2 ), array( '' ), array( 'str' ), array( array( 0, 1, 2 ) ), array( array( 'a' => 'b' ) ), array( array( 'a' => 'b' ) ), array( array( 'a' => 'b', 'x' => array( 'c' => 'd' ) ) ), ); } /** * Recursively convert arrays into stdClass * @param array|string|bool|int|float|null $value * @return stdClass|string|bool|int|float|null */ public static function toObject( $value ) { return !is_array( $value ) ? $value : (object) array_map( __METHOD__, $value ); } /** * @dataProvider provideParse * @param mixed $value */ public function testParse( $value ) { $expected = self::toObject( $value ); $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK ); $this->assertJson( $json ); $st = FormatJson::parse( $json ); $this->assertType( 'Status', $st ); $this->assertTrue( $st->isGood() ); $this->assertEquals( $expected, $st->getValue() ); $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC ); $this->assertType( 'Status', $st ); $this->assertTrue( $st->isGood() ); $this->assertEquals( $value, $st->getValue() ); } public static function provideParseTryFixing() { return array( array( "[,]", '[]' ), array( "[ , ]", '[]' ), array( "[ , }", false ), array( '[1],', false ), array( "[1,]", '[1]' ), array( "[1\n,]", '[1]' ), array( "[1,\n]", '[1]' ), array( "[1,]\n", '[1]' ), array( "[1\n,\n]\n", '[1]' ), array( '["a,",]', '["a,"]' ), array( "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ), array( '[[1,],[2,],[3,]]', false ), // I wish we could parse this, but would need quote parsing array( '[1,,]', false ), ); } /** * @dataProvider provideParseTryFixing * @param string $value * @param string|bool $expected */ public function testParseTryFixing( $value, $expected ) { $st = FormatJson::parse( $value, FormatJson::TRY_FIXING ); $this->assertType( 'Status', $st ); if ( $expected === false ) { $this->assertFalse( $st->isOK() ); } else { $this->assertFalse( $st->isGood() ); $this->assertTrue( $st->isOK() ); $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK ); $this->assertEquals( $expected, $val ); } } public static function provideParseErrors() { return array( array( 'aaa' ), array( '{"j": 1 ] }' ), ); } /** * @dataProvider provideParseErrors * @param mixed $value */ public function testParseErrors( $value ) { $st = FormatJson::parse( $value ); $this->assertType( 'Status', $st ); $this->assertFalse( $st->isOK() ); } /** * Generate a set of test cases for a particular combination of encoder options. * * @param array $unescapedGroups List of character groups to leave unescaped * @return array Arrays of unencoded strings and corresponding encoded strings */ private static function getEncodeTestCases( array $unescapedGroups ) { $groups = array( 'always' => array( // Forward slash (always unescaped) '/' => '/', // Control characters "\0" => '\u0000', "\x08" => '\b', "\t" => '\t', "\n" => '\n', "\r" => '\r', "\f" => '\f', "\x1f" => '\u001f', // representative example // Double quotes '"' => '\"', // Backslashes '\\' => '\\\\', '\\\\' => '\\\\\\\\', '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping // Line terminators "\xe2\x80\xa8" => '\u2028', "\xe2\x80\xa9" => '\u2029', ), 'unicode' => array( "\xc3\xa9" => '\u00e9', "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP ), 'xmlmeta' => array( '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits '>' => '\u003E', '&' => '\u0026', ), ); $cases = array(); foreach ( $groups as $name => $rules ) { $leaveUnescaped = in_array( $name, $unescapedGroups ); foreach ( $rules as $from => $to ) { $cases[] = array( $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' ); } } return $cases; } }