diff --git a/composer.json b/composer.json index dd1dfdb..59218c2 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "php": ">=7.4" }, "autoload": { - "classmap": ["jzon.inc.php", "jzon.inc.php"] + "classmap": ["jzon.inc.php"], + "files" : ["jzon.inc.php"] } } diff --git a/json.inc.php b/json.inc.php deleted file mode 100644 index eaa7dde..0000000 --- a/json.inc.php +++ /dev/null @@ -1,700 +0,0 @@ -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 index 3a027fe..3de86ab 100644 --- a/jzon.inc.php +++ b/jzon.inc.php @@ -1,5 +1,4 @@