PHP Classes

File: src/Parser.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   iaso PHP JSON Parser Library   src/Parser.php   Download  
File: src/Parser.php
Role: Class source
Content type: text/plain
Description: Class source
Class: iaso PHP JSON Parser Library
Parse JSON strings immune to hash-DoS attacks
Author: By
Last change:
Date: 1 year ago
Size: 13,473 bytes
 

Contents

Class file image Download
<?php declare(strict_types=1); namespace ParagonIE\Iaso; use ParagonIE\ConstantTime\Binary; use ParagonIE\Iaso\Result\Assoc; use ParagonIE\Iaso\Result\Bare; use ParagonIE\Iaso\Result\Ordered; /** * Class Parser * @package ParagonIE\Iaso */ class Parser { /** * @param string $json * @param Contract $contract * @return ResultSet */ public function parse(string $json, Contract $contract): ResultSet { $state = new ParseState( $json, $contract, 0, Binary::safeStrlen($json) ); while ($state->moreToRead()) { $state = $this->continueParsing($state); } if (empty($state->result)) { if (!empty($state->stack)) { $res = \array_pop($state->stack); $state->result = $res['obj']; } } return $state->result; } /** * @param ParseState $state * @return ParseState * @throws JSONError */ protected function continueParsing(ParseState $state): ParseState { $chr = $state->getChar(); switch ($chr) { case "\x09": case "\x0a": case "\x0d": case "\x20": // Continue on whitespace. while (\preg_match('#(\x09|\x0a|\x0d|\x20)#', $state->getChar())) { ++$state->pos; } return $state; case '{': // We're parsing an object. \array_push( $state->stack, [ 'type' => '{', 'begin' => $state->pos, 'end' => null, 'obj' => new Assoc() ] ); break; case '[': // We're parsing an array. \array_push( $state->stack, [ 'type' => '[', 'begin' => $state->pos, 'end' => null, 'obj' => new Ordered() ] ); break; case '/*': // Multiline comment $pos = \strpos($state->data, '*/', $state->pos); if ($pos === false) { throw new JSONError('Unclosed multiline comment'); } $state->pos = $pos + 1; break; case '//': // Single-line comment $pos = \strpos($state->data, "\n", $state->pos); if ($pos === false) { // Maybe this is before the ending? $state->pos = $state->length - 2; } break; case ']': $state = $this->closeArray($state); break; case '}': $state = $this->closeObject($state); break; case ',': // We don't expect a , in the middle of an object declaration $soft = $state->softPop(); if (!empty($soft['pending'])) { throw new JSONError('Unexpected ,'); } // Continue break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $state = $this->parseNumeric($state); break; case 'n': case 'N': if (\strtolower(Binary::safeSubstr($state->data, $state->pos, 4)) !== 'null') { throw new JSONError('Unexpected character "' . $chr . '"'); } $state = $this->parseNull($state); break; case 't': case 'T': if (\strtolower(Binary::safeSubstr($state->data, $state->pos, 4)) !== 'true') { throw new JSONError('Unexpected character "' . $chr . '"'); } $state = $this->parseBool($state, true); break; case 'f': case 'F': if (\strtolower(Binary::safeSubstr($state->data, $state->pos, 5)) !== 'false') { throw new JSONError('Unexpected character "' . $chr . '"'); } $state = $this->parseBool($state, false); break; case '"': // This can either be: // - A string (i.e. in an array) // - An object's key // - An object's value $state = $this->parseString($state); break; default: throw new JSONError('Unexpected character "' . $chr . '" (ASCII: ' . \ord($chr) . ')'); } ++$state->pos; return $state; } /** * @param ParseState $state * @throws JSONError * @return ParseState */ protected function closeArray(ParseState $state): ParseState { if (empty($state->stack)) { throw new JSONError('Cannot pop from empty stack'); } // We're closing out an array $pop = \array_pop($state->stack); if (empty($pop['type'])) { throw new JSONError('Corrupted stack'); } if ($pop['type'] !== '[') { throw new JSONError('Unexpected ]'); } return $state->passToParent($pop['obj']); } /** * @param ParseState $state * @throws JSONError * @return ParseState */ protected function closeObject(ParseState $state): ParseState { if (empty($state->stack)) { throw new JSONError('Cannot pop from empty stack'); } // We're closing out an object $pop = \array_pop($state->stack); if (empty($pop['type'])) { throw new JSONError('Corrupted stack'); } if ($pop['type'] !== '{') { throw new JSONError('Unexpected }'); } return $state->passToParent($pop['obj']); } /** * @param ParseState $state * @param bool $expect * @return ParseState * @throws JSONError */ protected function parseBool(ParseState $state, bool $expect = false): ParseState { if (empty($state->stack) && $state->pos === 0) { $state->pos = Binary::safeStrlen($state->data) - 1; $state->result = new Bare($expect, 'bool'); return $state; } $popped = $state->softPop(); if (empty($popped['type'])) { throw new JSONError('Corrupted stack'); } if ($popped['type'] === '[') { $popped['obj'][] = $expect; } elseif ($popped['type'] === '{') { $idx = $state->getLastIndex(); if (empty($state->stack[$idx]['pending'])) { // Uh oh. Dangling bool value. throw new JSONError('Unexpected boolean value'); } $key = $state->stack[$idx]['pending']; $state->stack[$idx]['obj'][$key] = $expect; // We don't need this anymore. Unset it. $state->stack[$idx]['pending'] = null; } else { throw new JSONError('Unexpected parent type'); } $state->pos += ($expect ? 4 : 5); return $state; } /** * @param ParseState $state * @return ParseState * @throws JSONError */ protected function parseNull(ParseState $state): ParseState { if (empty($state->stack) && $state->pos === 0) { $state->pos = Binary::safeStrlen($state->data) - 1; $state->result = new Bare(); return $state; } $popped = $state->softPop(); if (empty($popped['type'])) { throw new JSONError('Corrupted stack'); } if ($popped['type'] === '[') { $popped['obj'][] = null; } elseif ($popped['type'] === '{') { $idx = $state->getLastIndex(); if (empty($state->stack[$idx]['pending'])) { // Uh oh. Dangling bool value. throw new JSONError('Unexpected null value'); } $key = $state->stack[$idx]['pending']; $state->stack[$idx]['obj'][$key] = null; // We don't need this anymore. Unset it. $state->stack[$idx]['pending'] = null; } else { throw new JSONError('Unexpected parent type'); } $state->pos += 4; return $state; } /** * @param ParseState $state * @return ParseState * @throws JSONError */ protected function parseNumeric(ParseState $state): ParseState { $start = $pos = $state->pos; $period = false; $len = 0; do { ++$pos; if (!\ctype_digit($state->data[$pos])) { if ($state->data[$pos] === '.') { // Allow only one. if ($period) { throw new JSONError('Unexpected period (.) character.'); } $period = true; } else { // Stop parsing break; } } ++$len; } while ($pos < $state->length); $numeric = Binary::safeSubstr($state->data, $start, $len + 1); if ($period) { $result = (float) $numeric; } else { $result = (int) $numeric; } if (empty($state->stack) && $state->pos === 0) { $state->pos = Binary::safeStrlen($state->data) - 1; $state->result = new Bare($result, $period ? 'float' : 'int'); return $state; } $popped = $state->softPop(); if (empty($popped['type'])) { throw new JSONError('Corrupted stack'); } if ($popped['type'] === '[') { $popped['obj'][] = $result; } elseif ($popped['type'] === '{') { $idx = $state->getLastIndex(); if (empty($state->stack[$idx]['pending'])) { // Uh oh. Dangling numeric value. throw new JSONError('Unexpected numeric value'); } $key = $state->stack[$idx]['pending']; $state->stack[$idx]['obj'][$key] = $result; // We don't need this anymore. Unset it. $state->stack[$idx]['pending'] = null; } else { throw new JSONError('Unexpected parent type'); } $state->pos += $len; return $state; } /** * @param ParseState $state * @return ParseState * @throws JSONError */ protected function parseString(ParseState $state): ParseState { $start = $pos = $state->pos; $len = 0; do { ++$pos; ++$len; $search = \strpos($state->data, '"', $pos); if ($search !== false) { $pos = $search; $len = $pos - $start; } $escaped = $state->data[$pos - 1] === '\\'; } while ($escaped && $pos < $state->length); $idx = $state->getLastIndex(); if (empty($state->stack) && $state->pos === 0) { $string = \str_replace( '\"', '"', Binary::safeSubstr($state->data, $start + 1, $len - 1) ); $state->pos = Binary::safeStrlen($state->data) - 1; $state->result = new Bare($string, 'string'); return $state; } $popped = $state->softPop(); // This is the string we parsed. $string = \str_replace( '\"', '"', Binary::safeSubstr($state->data, $start + 1, $len - 1) ); // Strip whitespace while (\preg_match('/[\x09\x0a\x20]/', $state->data[$pos + 1]) && $pos < $state->length) { ++$pos; } if ($popped['type'] === '{') { if (isset($popped['pending'])) { // We're finalizing this entry with another string. if ($state->data[$pos + 1] === ':') { throw new JSONError('Unexpected :'); } // Assign the value. $key = $state->stack[$idx]['pending']; $state->stack[$idx]['obj'][$key] = $string; // We don't need this anymore. Unset it. $state->stack[$idx]['pending'] = null; } else { // We're expecting a value for this later. if ($state->data[$pos + 1] !== ':') { throw new JSONError('Expected ":", got "' . $state->data[$pos + 1] . '" instead.'); } $state->stack[$idx]['pending'] = $string; ++$pos; } } elseif ($popped['type'] === '[') { // We don't expect key:value pairs inside of a [] array if ($state->data[$pos + 1] === ':') { throw new JSONError('Unexpected :'); } $state->data[$idx]['obj'][] = $string; } $state->pos = $pos; return $state; } }