commit 1c02350e6aa4b476f97da097975a75ddc224a825 Author: Pavel Shevaev Date: Mon May 16 14:19:18 2022 +0300 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/json.inc.php b/json.inc.php new file mode 100644 index 0000000..eaa7dde --- /dev/null +++ b/json.inc.php @@ -0,0 +1,700 @@ +lint($json); + * // returns parsed json, like json_decode does, but slower, throws exceptions on failure. + * $parser->parse($json); + * + * Ported from https://github.com/zaach/jsonlint + */ +class JsonParser +{ + const DETECT_KEY_CONFLICTS = 1; + const ALLOW_DUPLICATE_KEYS = 2; + const PARSE_TO_ASSOC = 4; + + private $lexer; + + private $flags; + private $stack; + private $vstack; // semantic value stack + private $lstack; // location stack + + private $symbols = array( + 'error' => 2, + 'JSONString' => 3, + 'STRING' => 4, + 'JSONNumber' => 5, + 'NUMBER' => 6, + 'JSONNullLiteral' => 7, + 'NULL' => 8, + 'JSONBooleanLiteral' => 9, + 'TRUE' => 10, + 'FALSE' => 11, + 'JSONText' => 12, + 'JSONValue' => 13, + 'EOF' => 14, + 'JSONObject' => 15, + 'JSONArray' => 16, + '{' => 17, + '}' => 18, + 'JSONMemberList' => 19, + 'JSONMember' => 20, + ':' => 21, + ',' => 22, + '[' => 23, + ']' => 24, + 'JSONElementList' => 25, + '$accept' => 0, + '$end' => 1, + ); + + private $terminals_ = array( + 2 => "error", + 4 => "STRING", + 6 => "NUMBER", + 8 => "NULL", + 10 => "TRUE", + 11 => "FALSE", + 14 => "EOF", + 17 => "{", + 18 => "}", + 21 => ":", + 22 => ",", + 23 => "[", + 24 => "]", + ); + + private $productions_ = array( + 0, + array(3, 1), + array(5, 1), + array(7, 1), + array(9, 1), + array(9, 1), + array(12, 2), + array(13, 1), + array(13, 1), + array(13, 1), + array(13, 1), + array(13, 1), + array(13, 1), + array(15, 2), + array(15, 3), + array(20, 3), + array(19, 1), + array(19, 3), + array(16, 2), + array(16, 3), + array(25, 1), + array(25, 3) + ); + + private $table = array(array(3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 12 => 1, 13 => 2, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 1 => array(3)), array( 14 => array(1,16)), array( 14 => array(2,7), 18 => array(2,7), 22 => array(2,7), 24 => array(2,7)), array( 14 => array(2,8), 18 => array(2,8), 22 => array(2,8), 24 => array(2,8)), array( 14 => array(2,9), 18 => array(2,9), 22 => array(2,9), 24 => array(2,9)), array( 14 => array(2,10), 18 => array(2,10), 22 => array(2,10), 24 => array(2,10)), array( 14 => array(2,11), 18 => array(2,11), 22 => array(2,11), 24 => array(2,11)), array( 14 => array(2,12), 18 => array(2,12), 22 => array(2,12), 24 => array(2,12)), array( 14 => array(2,3), 18 => array(2,3), 22 => array(2,3), 24 => array(2,3)), array( 14 => array(2,4), 18 => array(2,4), 22 => array(2,4), 24 => array(2,4)), array( 14 => array(2,5), 18 => array(2,5), 22 => array(2,5), 24 => array(2,5)), array( 14 => array(2,1), 18 => array(2,1), 21 => array(2,1), 22 => array(2,1), 24 => array(2,1)), array( 14 => array(2,2), 18 => array(2,2), 22 => array(2,2), 24 => array(2,2)), array( 3 => 20, 4 => array(1,12), 18 => array(1,17), 19 => 18, 20 => 19 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 23, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15), 24 => array(1,21), 25 => 22 ), array( 1 => array(2,6)), array( 14 => array(2,13), 18 => array(2,13), 22 => array(2,13), 24 => array(2,13)), array( 18 => array(1,24), 22 => array(1,25)), array( 18 => array(2,16), 22 => array(2,16)), array( 21 => array(1,26)), array( 14 => array(2,18), 18 => array(2,18), 22 => array(2,18), 24 => array(2,18)), array( 22 => array(1,28), 24 => array(1,27)), array( 22 => array(2,20), 24 => array(2,20)), array( 14 => array(2,14), 18 => array(2,14), 22 => array(2,14), 24 => array(2,14)), array( 3 => 20, 4 => array(1,12), 20 => 29 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 30, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 14 => array(2,19), 18 => array(2,19), 22 => array(2,19), 24 => array(2,19)), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 31, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 18 => array(2,17), 22 => array(2,17)), array( 18 => array(2,15), 22 => array(2,15)), array( 22 => array(2,21), 24 => array(2,21)), + ); + + private $defaultActions = array( + 16 => array(2, 6) + ); + + /** + * @param string $input JSON string + * @return null|ParsingException null if no error is found, a ParsingException containing all details otherwise + */ + public function lint($input) + { + try { + $this->parse($input); + } catch (JsonParsingException $e) { + return $e; + } + } + + /** + * @param string $input JSON string + * @return mixed + * @throws ParsingException + */ + public function parse($input, $flags = 0) + { + $this->failOnBOM($input); + + $this->flags = $flags; + + $this->stack = array(0); + $this->vstack = array(null); + $this->lstack = array(); + + $yytext = ''; + $yylineno = 0; + $yyleng = 0; + $recovering = 0; + $TERROR = 2; + $EOF = 1; + + $this->lexer = new JsonLexer(); + $this->lexer->setInput($input); + + $yyloc = $this->lexer->yylloc; + $this->lstack[] = $yyloc; + + $symbol = null; + $preErrorSymbol = null; + $state = null; + $action = null; + $a = null; + $r = null; + $yyval = new stdClass; + $p = null; + $len = null; + $newState = null; + $expected = null; + $errStr = null; + + while (true) { + // retreive state number from top of stack + $state = $this->stack[count($this->stack)-1]; + + // use default actions if available + if (isset($this->defaultActions[$state])) { + $action = $this->defaultActions[$state]; + } else { + if ($symbol == null) { + $symbol = $this->lex(); + } + // read action for current state and first input + $action = isset($this->table[$state][$symbol]) ? $this->table[$state][$symbol] : false; + } + + // handle parse error + if (!$action || !$action[0]) { + if (!$recovering) { + // Report error + $expected = array(); + foreach ($this->table[$state] as $p => $ignore) { + if (isset($this->terminals_[$p]) && $p > 2) { + $expected[] = "'" . $this->terminals_[$p] . "'"; + } + } + + $message = null; + if (in_array("'STRING'", $expected) && in_array(substr($this->lexer->match, 0, 1), array('"', "'"))) { + $message = "Invalid string"; + if ("'" === substr($this->lexer->match, 0, 1)) { + $message .= ", it appears you used single quotes instead of double quotes"; + } elseif (preg_match('{".+?(\\\\[^"bfnrt/\\\\u])}', $this->lexer->getUpcomingInput(), $match)) { + $message .= ", it appears you have an unescaped backslash at: ".$match[1]; + } elseif (preg_match('{"(?:[^"]+|\\\\")*$}m', $this->lexer->getUpcomingInput())) { + $message .= ", it appears you forgot to terminated the string, or attempted to write a multiline string which is invalid"; + } + } + + $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; + $errStr .= $this->lexer->showPosition() . "\n"; + if ($message) { + $errStr .= $message; + } else { + $errStr .= (count($expected) > 1) ? "Expected one of: " : "Expected: "; + $errStr .= implode(', ', $expected); + } + + if (',' === substr(trim($this->lexer->getPastInput()), -1)) { + $errStr .= " - It appears you have an extra trailing comma"; + } + + $this->parseError($errStr, array( + 'text' => $this->lexer->match, + 'token' => !empty($this->terminals_[$symbol]) ? $this->terminals_[$symbol] : $symbol, + 'line' => $this->lexer->yylineno, + 'loc' => $yyloc, + 'expected' => $expected, + )); + } + + // just recovered from another error + if ($recovering == 3) { + if ($symbol == $EOF) { + throw new JsonParsingException($errStr ?: 'Parsing halted.'); + } + + // discard current lookahead and grab another + $yyleng = $this->lexer->yyleng; + $yytext = $this->lexer->yytext; + $yylineno = $this->lexer->yylineno; + $yyloc = $this->lexer->yylloc; + $symbol = $this->lex(); + } + + // try to recover from error + while (true) { + // check for error recovery rule in this state + if (array_key_exists($TERROR, $this->table[$state])) { + break; + } + if ($state == 0) { + throw new JsonParsingException($errStr ?: 'Parsing halted.'); + } + $this->popStack(1); + $state = $this->stack[count($this->stack)-1]; + } + + $preErrorSymbol = $symbol; // save the lookahead token + $symbol = $TERROR; // insert generic error symbol as new lookahead + $state = $this->stack[count($this->stack)-1]; + $action = isset($this->table[$state][$TERROR]) ? $this->table[$state][$TERROR] : false; + $recovering = 3; // allow 3 real symbols to be shifted before reporting a new error + } + + // this shouldn't happen, unless resolve defaults are off + if (is_array($action[0]) && count($action) > 1) { + throw new JsonParsingException('Parse Error: multiple actions possible at state: ' . $state . ', token: ' . $symbol); + } + + switch ($action[0]) { + case 1: // shift + $this->stack[] = $symbol; + $this->vstack[] = $this->lexer->yytext; + $this->lstack[] = $this->lexer->yylloc; + $this->stack[] = $action[1]; // push state + $symbol = null; + if (!$preErrorSymbol) { // normal execution/no error + $yyleng = $this->lexer->yyleng; + $yytext = $this->lexer->yytext; + $yylineno = $this->lexer->yylineno; + $yyloc = $this->lexer->yylloc; + if ($recovering > 0) { + $recovering--; + } + } else { // error just occurred, resume old lookahead f/ before error + $symbol = $preErrorSymbol; + $preErrorSymbol = null; + } + break; + + case 2: // reduce + $len = $this->productions_[$action[1]][1]; + + // perform semantic action + $yyval->token = $this->vstack[count($this->vstack) - $len]; // default to $$ = $1 + // default location, uses first token for firsts, last for lasts + $yyval->store = array( // _$ = store + 'first_line' => $this->lstack[count($this->lstack) - ($len ?: 1)]['first_line'], + 'last_line' => $this->lstack[count($this->lstack) - 1]['last_line'], + 'first_column' => $this->lstack[count($this->lstack) - ($len ?: 1)]['first_column'], + 'last_column' => $this->lstack[count($this->lstack) - 1]['last_column'], + ); + $r = $this->performAction($yyval, $yytext, $yyleng, $yylineno, $action[1], $this->vstack, $this->lstack); + + if (!$r instanceof JsonUndefined) { + return $r; + } + + if ($len) { + $this->popStack($len); + } + + $this->stack[] = $this->productions_[$action[1]][0]; // push nonterminal (reduce) + $this->vstack[] = $yyval->token; + $this->lstack[] = $yyval->store; + $newState = $this->table[$this->stack[count($this->stack)-2]][$this->stack[count($this->stack)-1]]; + $this->stack[] = $newState; + break; + + case 3: // accept + + return true; + } + } + + return true; + } + + protected function parseError($str, $hash) + { + throw new JsonParsingException($str, $hash); + } + + // $$ = $tokens // needs to be passed by ref? + // $ = $token + // _$ removed, useless? + private function performAction(stdClass $yyval, $yytext, $yyleng, $yylineno, $yystate, &$tokens) + { + // $0 = $len + $len = count($tokens) - 1; + switch ($yystate) { + case 1: + $yytext = preg_replace_callback('{(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4})}', array($this, 'stringInterpolation'), $yytext); + $yyval->token = $yytext; + break; + case 2: + if (strpos($yytext, 'e') !== false || strpos($yytext, 'E') !== false) { + $yyval->token = floatval($yytext); + } else { + $yyval->token = strpos($yytext, '.') === false ? intval($yytext) : floatval($yytext); + } + break; + case 3: + $yyval->token = null; + break; + case 4: + $yyval->token = true; + break; + case 5: + $yyval->token = false; + break; + case 6: + return $yyval->token = $tokens[$len-1]; + case 13: + if ($this->flags & self::PARSE_TO_ASSOC) { + $yyval->token = array(); + } else { + $yyval->token = new stdClass; + } + break; + case 14: + $yyval->token = $tokens[$len-1]; + break; + case 15: + $yyval->token = array($tokens[$len-2], $tokens[$len]); + break; + case 16: + $property = $tokens[$len][0] === '' ? '_empty_' : $tokens[$len][0]; + if ($this->flags & self::PARSE_TO_ASSOC) { + $yyval->token = array(); + $yyval->token[$property] = $tokens[$len][1]; + } else { + $yyval->token = new stdClass; + $yyval->token->$property = $tokens[$len][1]; + } + break; + case 17: + if ($this->flags & self::PARSE_TO_ASSOC) { + $yyval->token =& $tokens[$len-2]; + $key = $tokens[$len][0]; + if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2][$key])) { + $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; + $errStr .= $this->lexer->showPosition() . "\n"; + $errStr .= "Duplicate key: ".$tokens[$len][0]; + throw new JsonParsingException($errStr); + } elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2][$key])) { + $duplicateCount = 1; + do { + $duplicateKey = $key . '.' . $duplicateCount++; + } while (isset($tokens[$len-2][$duplicateKey])); + $key = $duplicateKey; + } + $tokens[$len-2][$key] = $tokens[$len][1]; + } else { + $yyval->token = $tokens[$len-2]; + $key = $tokens[$len][0] === '' ? '_empty_' : $tokens[$len][0]; + if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2]->{$key})) { + $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; + $errStr .= $this->lexer->showPosition() . "\n"; + $errStr .= "Duplicate key: ".$tokens[$len][0]; + throw new JsonParsingException($errStr); + } elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2]->{$key})) { + $duplicateCount = 1; + do { + $duplicateKey = $key . '.' . $duplicateCount++; + } while (isset($tokens[$len-2]->$duplicateKey)); + $key = $duplicateKey; + } + $tokens[$len-2]->$key = $tokens[$len][1]; + } + break; + case 18: + $yyval->token = array(); + break; + case 19: + $yyval->token = $tokens[$len-1]; + break; + case 20: + $yyval->token = array($tokens[$len]); + break; + case 21: + $tokens[$len-2][] = $tokens[$len]; + $yyval->token = $tokens[$len-2]; + break; + } + + return new JsonUndefined(); + } + + private function stringInterpolation($match) + { + switch ($match[0]) { + case '\\\\': + return '\\'; + case '\"': + return '"'; + case '\b': + return chr(8); + case '\f': + return chr(12); + case '\n': + return "\n"; + case '\r': + return "\r"; + case '\t': + return "\t"; + case '\/': + return "/"; + default: + return html_entity_decode('&#x'.ltrim(substr($match[0], 2), '0').';', 0, 'UTF-8'); + } + } + + private function popStack($n) + { + $this->stack = array_slice($this->stack, 0, - (2 * $n)); + $this->vstack = array_slice($this->vstack, 0, - $n); + $this->lstack = array_slice($this->lstack, 0, - $n); + } + + private function lex() + { + $token = $this->lexer->lex() ?: 1; // $end = 1 + // if token isn't its numeric value, convert + if (!is_numeric($token)) { + $token = isset($this->symbols[$token]) ? $this->symbols[$token] : $token; + } + + return $token; + } + + private function failOnBOM($input) + { + // UTF-8 ByteOrderMark sequence + $bom = "\xEF\xBB\xBF"; + + if (substr($input, 0, 3) === $bom) { + $this->parseError("BOM detected, make sure your input does not include a Unicode Byte-Order-Mark", array()); + } + } +} + +class JsonParsingException extends Exception +{ + protected $details; + + public function __construct($message, $details = array()) + { + $this->details = $details; + parent::__construct($message); + } + + public function getDetails() + { + return $this->details; + } +} + +class JsonLexer +{ + private $EOF = 1; + private $rules = array( + 0 => '/^\s+/', + 1 => '/^-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][+-]?[0-9]+)?\b/', + 2 => '{^"(\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x09\x0a-\x1f\\\\"])*"}', + 3 => '/^\{/', + 4 => '/^\}/', + 5 => '/^\[/', + 6 => '/^\]/', + 7 => '/^,/', + 8 => '/^:/', + 9 => '/^true\b/', + 10 => '/^false\b/', + 11 => '/^null\b/', + 12 => '/^$/', + 13 => '/^./', + ); + + private $conditions = array( + "INITIAL" => array( + "rules" => array(0,1,2,3,4,5,6,7,8,9,10,11,12,13), + "inclusive" => true, + ), + ); + + private $conditionStack; + private $input; + private $more; + private $done; + private $matched; + + public $match; + public $yylineno; + public $yyleng; + public $yytext; + public $yylloc; + + public function lex() + { + $r = $this->next(); + if (!$r instanceof JsonUndefined) { + return $r; + } + + return $this->lex(); + } + + public function setInput($input) + { + $this->input = $input; + $this->more = false; + $this->done = false; + $this->yylineno = $this->yyleng = 0; + $this->yytext = $this->matched = $this->match = ''; + $this->conditionStack = array('INITIAL'); + $this->yylloc = array('first_line' => 1, 'first_column' => 0, 'last_line' => 1, 'last_column' => 0); + + return $this; + } + + public function showPosition() + { + $pre = str_replace("\n", '', $this->getPastInput()); + $c = str_repeat('-', max(0, strlen($pre) - 1)); // new Array(pre.length + 1).join("-"); + + return $pre . str_replace("\n", '', $this->getUpcomingInput()) . "\n" . $c . "^"; + } + + public function getPastInput() + { + $past = substr($this->matched, 0, strlen($this->matched) - strlen($this->match)); + + return (strlen($past) > 120 ? '...' : '') . substr($past, -120); + } + + public function getUpcomingInput() + { + $next = $this->match; + if (strlen($next) < 120) { + $next .= substr($this->input, 0, 120 - strlen($next)); + } + + return substr($next, 0, 120) . (strlen($next) > 120 ? '...' : ''); + } + + protected function parseError($str, $hash) + { + throw new Exception($str); + } + + private function next() + { + if ($this->done) { + return $this->EOF; + } + if (!$this->input) { + $this->done = true; + } + + $token = null; + $match = null; + $col = null; + $lines = null; + + if (!$this->more) { + $this->yytext = ''; + $this->match = ''; + } + + $rules = $this->getCurrentRules(); + $rulesLen = count($rules); + + for ($i=0; $i < $rulesLen; $i++) { + if (preg_match($this->rules[$rules[$i]], $this->input, $match)) { + preg_match_all('/\n.*/', $match[0], $lines); + $lines = $lines[0]; + if ($lines) { + $this->yylineno += count($lines); + } + + $this->yylloc = array( + 'first_line' => $this->yylloc['last_line'], + 'last_line' => $this->yylineno+1, + 'first_column' => $this->yylloc['last_column'], + 'last_column' => $lines ? strlen($lines[count($lines) - 1]) - 1 : $this->yylloc['last_column'] + strlen($match[0]), + ); + $this->yytext .= $match[0]; + $this->match .= $match[0]; + $this->yyleng = strlen($this->yytext); + $this->more = false; + $this->input = substr($this->input, strlen($match[0])); + $this->matched .= $match[0]; + $token = $this->performAction($rules[$i], $this->conditionStack[count($this->conditionStack)-1]); + if ($token) { + return $token; + } + + return new JsonUndefined(); + } + } + + if ($this->input === "") { + return $this->EOF; + } + + $this->parseError( + 'Lexical error on line ' . ($this->yylineno+1) . ". Unrecognized text.\n" . $this->showPosition(), + array( + 'text' => "", + 'token' => null, + 'line' => $this->yylineno, + ) + ); + } + + private function getCurrentRules() + { + return $this->conditions[$this->conditionStack[count($this->conditionStack)-1]]['rules']; + } + + private function performAction($avoiding_name_collisions, $YY_START) + { + switch ($avoiding_name_collisions) { + case 0:/* skip whitespace */ + break; + case 1: + return 6; + break; + case 2: + $this->yytext = substr($this->yytext, 1, $this->yyleng-2); + + return 4; + case 3: + return 17; + case 4: + return 18; + case 5: + return 23; + case 6: + return 24; + case 7: + return 22; + case 8: + return 21; + case 9: + return 10; + case 10: + return 11; + case 11: + return 8; + case 12: + return 14; + case 13: + return 'INVALID'; + } + } +} + +class JsonUndefined +{ +} diff --git a/jzon.inc.php b/jzon.inc.php new file mode 100644 index 0000000..3a027fe --- /dev/null +++ b/jzon.inc.php @@ -0,0 +1,355 @@ + $context_chars ? '...' : '') . substr($past, -$context_chars); +} + +function jzon_get_upcoming_input($c, $in, $context_chars) +{ + $next = substr($in, $c+1); + return substr($next, 0, $context_chars) . (strlen($next) > $context_chars ? '...' : ''); +} + +class jzonParser +{ + const ERR_CONTEXT_CHARS = 200; + + private $in; + private $len; + private $c = 0; + + static $ORD_SPACE; + static $ORD_0; + static $ORD_9; + + function __construct($input) + { + $this->in = $input; + $this->c = 0; + $this->len = strlen($this->in); + + self::$ORD_SPACE = ord(' '); + self::$ORD_0 = ord('0'); + self::$ORD_9 = ord('9'); + } + + function parse() + { + $out = null; + if($this->c < $this->len && $this->in[$this->c] == '[') + $this->parse_array($out); + else + $this->parse_object($out, true); + $this->skip_whitespace(); + if($this->c != $this->len) + $this->_error("Trailing content"); + return $out; + } + + private function parse_value(&$out) + { + $this->skip_whitespace(); + $ch = $this->in[$this->c]; + + switch($ch) + { + case '{': $this->parse_object($out, false); break; + case '[': $this->parse_array($out); break; + case '"': $this->parse_string($out); break; + case '-': $this->parse_number($out); break; + case 'f': $this->parse_false($out); break; + case 't': $this->parse_true($out); break; + case 'n': $this->parse_null($out); break; + default: + ord($ch) >= self::$ORD_0 && ord($ch) <= self::$ORD_9 ? + $this->parse_number($out) : + $this->_error("Not expected symbol"); + } + } + + private function _error($error) + { + if($this->c < $this->len) + throw new Exception("Parse error: $error\n" . jzon_show_position($this->c, $this->in, self::ERR_CONTEXT_CHARS)); + else + throw new Exception("Parse error: $error\n" . jzon_show_position($this->len-1, $this->in, self::ERR_CONTEXT_CHARS)); + } + + private function skip_whitespace() + { + //while($this->c < $this->len) + //{ + while($this->c < $this->len && (ord($ch = $this->in[$this->c]) <= self::$ORD_SPACE || $ch == ',')) + ++$this->c; + + // // skip line comment. + // if($this->c < $this->len && $this->in[$this->c] === '/' && $this->c+1 < $this->len && $this->in[$this->c+1] === '/') + // { + // ++$this->c; + // while($this->c < $this->len && $this->in[$this->c] != "\n") + // ++$this->c; + // } + // else + // break; + //} + } + + private function parse_object(&$out, $root_object) + { + if(isset($this->in[$this->c]) && $this->in[$this->c] == '{') + ++$this->c; + else if(!$root_object) + $this->_error("No root object"); + + $this->skip_whitespace(); + + $out = array(); + // Empty object. + if(isset($this->in[$this->c]) && $this->in[$this->c] == '}') + { + ++$this->c; + return; + } + + while($this->c < $this->len) + { + $this->skip_whitespace(); + $key = $this->parse_keyname(); + $this->skip_whitespace(); + + if($key === null) + $this->_error("Bad key"); + if($this->in[$this->c] != ':') + $this->_error("':' expected"); + + ++$this->c; + $value = null; + $this->parse_value($value); + + $out[$key] = $value; + + $this->skip_whitespace(); + + if($this->c < $this->len && $this->in[$this->c] == '}') + { + ++$this->c; + break; + } + } + return; + } + + private function parse_array(&$out) + { + if($this->in[$this->c] != '[') + $this->_error("'[' expected"); + + ++$this->c; + $this->skip_whitespace(); + + $out = array(); + + // Empty array. + if($this->c < $this->len && $this->in[$this->c] == ']') + { + ++$this->c; + return; + } + + while($this->c < $this->len) + { + $this->skip_whitespace(); + $value = null; + $this->parse_value($value); + + $out[] = $value; + + $this->skip_whitespace(); + + if($this->c < $this->len && $this->in[$this->c] == ']') + { + ++$this->c; + break; + } + } + } + + private function parse_keyname() + { + if($this->in[$this->c] == '"') + { + $end_char = '"'; + ++$this->c; + } + else + $end_char = ':'; + + $start = $this->c; + + while($this->c < $this->len) + { + $c = $this->in[$this->c]; + if($c == $end_char) + { + $end = $this->c; + if($end_char == '"') + ++$this->c; + return substr($this->in, $start, $end - $start); + } + else if(ord($c) <= self::$ORD_SPACE) + $this->_error("Found bad key character '$c' with ord=" . ord($c). " in position {$this->c}. HINT: if you ensure about file's syntax - check the file's encoding"); + + ++$this->c; + } + + return null; + } + + private function parse_string(&$out) + { + $str = $this->parse_string_internal(); + + if($str === null) + $this->_error("Bad string"); + + $out = $str; + } + + private function parse_string_internal() + { + if($this->in[$this->c] != '"') + return null; + + //if(is_multiline_string_quotes(*input)) + // return parse_multiline_string(input, allocator); + + ++$this->c; + $start = $this->c; + + $prev = ''; + while($this->c < $this->len) + { + if($this->in[$this->c] == '"' && $prev != '\\') + { + $end = $this->c; + ++$this->c; + return substr($this->in, $start, $end - $start); + } + $prev = $this->in[$this->c]; + ++$this->c; + } + + return null; + } + + private function parse_number(&$out) + { + $is_float = false; + $start = $this->c; + + if($this->in[$this->c] == '-') + ++$this->c; + + while($this->c < $this->len && ord($this->in[$this->c]) >= self::$ORD_0 && ord($this->in[$this->c]) <= self::$ORD_9) + ++$this->c; + + if($this->c < $this->len && $this->in[$this->c] == '.') + { + $is_float = true; + ++$this->c; + + while($this->c < $this->len && ord($this->in[$this->c]) >= self::$ORD_0 && ord($this->in[$this->c]) <= self::$ORD_9) + ++$this->c; + } + + if($this->c < $this->len && ($this->in[$this->c] == 'e' || $this->in[$this->c] == 'E')) + { + $is_float = true; + ++$this->c; + + if($this->c < $this->len && ($this->in[$this->c] == '-' || $this->in[$this->c] == '+')) + ++$this->c; + + while($this->c < $this->len && ord($this->in[$this->c]) >= self::$ORD_0 && ord($this->in[$this->c]) <= self::$ORD_9) + ++$this->c; + } + + if($is_float) + $out = 1*substr($this->in, $start, $this->c - $start); + else + $out = (int)(1*substr($this->in, $start, $this->c - $start)); + } + + private function parse_true(&$out) + { + if(substr_compare($this->in, 'true', $this->c, 4) == 0) + { + $out = true; + $this->c += 4; + } + else + $this->_error("'true' expected"); + } + + private function parse_false(&$out) + { + if(substr_compare($this->in, 'false', $this->c, 5) == 0) + { + $out = false; + $this->c += 5; + } + else + $this->_error("'false' expected"); + } + + private function parse_null(&$out) + { + if(substr_compare($this->in, 'null', $this->c, 4) == 0) + { + $out = null; + $this->c += 4; + } + else + $this->_error("'null' expected"); + } + +} + +function jzon_parse($str) +{ + //NOTE: using super fast built-in implementation and making a gracefull fallback to a slower and + // more relaxed implementation + $res = json_decode($str, true); + if(is_array($res)) + return $res; + + //NOTE: only allowing extension implementation if versions match + if(JZON_EXT_VERSION === JZON_VERSION) + { + list($ok, $err, $err_pos, $res) = jzon_parse_c($str); + if(!$ok) + throw new Exception($err . "\n" . jzon_show_position($err_pos, $str, jzonParser::ERR_CONTEXT_CHARS)); + return $res; + } + else + { + $p = new jzonParser($str); + return $p->parse(); + } +}