<?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;
    }
}
 
  |