| 1 |
<?php |
| 2 |
|
| 3 |
namespace Dudlewebs\WPMCS\s3\JmesPath; |
| 4 |
|
| 5 |
/** |
| 6 |
* Tokenizes JMESPath expressions |
| 7 |
*/ |
| 8 |
class Lexer |
| 9 |
{ |
| 10 |
const T_DOT = 'dot'; |
| 11 |
const T_STAR = 'star'; |
| 12 |
const T_COMMA = 'comma'; |
| 13 |
const T_COLON = 'colon'; |
| 14 |
const T_CURRENT = 'current'; |
| 15 |
const T_EXPREF = 'expref'; |
| 16 |
const T_LPAREN = 'lparen'; |
| 17 |
const T_RPAREN = 'rparen'; |
| 18 |
const T_LBRACE = 'lbrace'; |
| 19 |
const T_RBRACE = 'rbrace'; |
| 20 |
const T_LBRACKET = 'lbracket'; |
| 21 |
const T_RBRACKET = 'rbracket'; |
| 22 |
const T_FLATTEN = 'flatten'; |
| 23 |
const T_IDENTIFIER = 'identifier'; |
| 24 |
const T_NUMBER = 'number'; |
| 25 |
const T_QUOTED_IDENTIFIER = 'quoted_identifier'; |
| 26 |
const T_UNKNOWN = 'unknown'; |
| 27 |
const T_PIPE = 'pipe'; |
| 28 |
const T_OR = 'or'; |
| 29 |
const T_AND = 'and'; |
| 30 |
const T_NOT = 'not'; |
| 31 |
const T_FILTER = 'filter'; |
| 32 |
const T_LITERAL = 'literal'; |
| 33 |
const T_EOF = 'eof'; |
| 34 |
const T_COMPARATOR = 'comparator'; |
| 35 |
const STATE_IDENTIFIER = 0; |
| 36 |
const STATE_NUMBER = 1; |
| 37 |
const STATE_SINGLE_CHAR = 2; |
| 38 |
const STATE_WHITESPACE = 3; |
| 39 |
const STATE_STRING_LITERAL = 4; |
| 40 |
const STATE_QUOTED_STRING = 5; |
| 41 |
const STATE_JSON_LITERAL = 6; |
| 42 |
const STATE_LBRACKET = 7; |
| 43 |
const STATE_PIPE = 8; |
| 44 |
const STATE_LT = 9; |
| 45 |
const STATE_GT = 10; |
| 46 |
const STATE_EQ = 11; |
| 47 |
const STATE_NOT = 12; |
| 48 |
const STATE_AND = 13; |
| 49 |
/** @var array We know what token we are consuming based on each char */ |
| 50 |
private static $transitionTable = ['<' => self::STATE_LT, '>' => self::STATE_GT, '=' => self::STATE_EQ, '!' => self::STATE_NOT, '[' => self::STATE_LBRACKET, '|' => self::STATE_PIPE, '&' => self::STATE_AND, '`' => self::STATE_JSON_LITERAL, '"' => self::STATE_QUOTED_STRING, "'" => self::STATE_STRING_LITERAL, '-' => self::STATE_NUMBER, '0' => self::STATE_NUMBER, '1' => self::STATE_NUMBER, '2' => self::STATE_NUMBER, '3' => self::STATE_NUMBER, '4' => self::STATE_NUMBER, '5' => self::STATE_NUMBER, '6' => self::STATE_NUMBER, '7' => self::STATE_NUMBER, '8' => self::STATE_NUMBER, '9' => self::STATE_NUMBER, ' ' => self::STATE_WHITESPACE, "\t" => self::STATE_WHITESPACE, "\n" => self::STATE_WHITESPACE, "\r" => self::STATE_WHITESPACE, '.' => self::STATE_SINGLE_CHAR, '*' => self::STATE_SINGLE_CHAR, ']' => self::STATE_SINGLE_CHAR, ',' => self::STATE_SINGLE_CHAR, ':' => self::STATE_SINGLE_CHAR, '@' => self::STATE_SINGLE_CHAR, '(' => self::STATE_SINGLE_CHAR, ')' => self::STATE_SINGLE_CHAR, '{' => self::STATE_SINGLE_CHAR, '}' => self::STATE_SINGLE_CHAR, '_' => self::STATE_IDENTIFIER, 'A' => self::STATE_IDENTIFIER, 'B' => self::STATE_IDENTIFIER, 'C' => self::STATE_IDENTIFIER, 'D' => self::STATE_IDENTIFIER, 'E' => self::STATE_IDENTIFIER, 'F' => self::STATE_IDENTIFIER, 'G' => self::STATE_IDENTIFIER, 'H' => self::STATE_IDENTIFIER, 'I' => self::STATE_IDENTIFIER, 'J' => self::STATE_IDENTIFIER, 'K' => self::STATE_IDENTIFIER, 'L' => self::STATE_IDENTIFIER, 'M' => self::STATE_IDENTIFIER, 'N' => self::STATE_IDENTIFIER, 'O' => self::STATE_IDENTIFIER, 'P' => self::STATE_IDENTIFIER, 'Q' => self::STATE_IDENTIFIER, 'R' => self::STATE_IDENTIFIER, 'S' => self::STATE_IDENTIFIER, 'T' => self::STATE_IDENTIFIER, 'U' => self::STATE_IDENTIFIER, 'V' => self::STATE_IDENTIFIER, 'W' => self::STATE_IDENTIFIER, 'X' => self::STATE_IDENTIFIER, 'Y' => self::STATE_IDENTIFIER, 'Z' => self::STATE_IDENTIFIER, 'a' => self::STATE_IDENTIFIER, 'b' => self::STATE_IDENTIFIER, 'c' => self::STATE_IDENTIFIER, 'd' => self::STATE_IDENTIFIER, 'e' => self::STATE_IDENTIFIER, 'f' => self::STATE_IDENTIFIER, 'g' => self::STATE_IDENTIFIER, 'h' => self::STATE_IDENTIFIER, 'i' => self::STATE_IDENTIFIER, 'j' => self::STATE_IDENTIFIER, 'k' => self::STATE_IDENTIFIER, 'l' => self::STATE_IDENTIFIER, 'm' => self::STATE_IDENTIFIER, 'n' => self::STATE_IDENTIFIER, 'o' => self::STATE_IDENTIFIER, 'p' => self::STATE_IDENTIFIER, 'q' => self::STATE_IDENTIFIER, 'r' => self::STATE_IDENTIFIER, 's' => self::STATE_IDENTIFIER, 't' => self::STATE_IDENTIFIER, 'u' => self::STATE_IDENTIFIER, 'v' => self::STATE_IDENTIFIER, 'w' => self::STATE_IDENTIFIER, 'x' => self::STATE_IDENTIFIER, 'y' => self::STATE_IDENTIFIER, 'z' => self::STATE_IDENTIFIER]; |
| 51 |
/** @var array Valid identifier characters after first character */ |
| 52 |
private $validIdentifier = ['A' => \true, 'B' => \true, 'C' => \true, 'D' => \true, 'E' => \true, 'F' => \true, 'G' => \true, 'H' => \true, 'I' => \true, 'J' => \true, 'K' => \true, 'L' => \true, 'M' => \true, 'N' => \true, 'O' => \true, 'P' => \true, 'Q' => \true, 'R' => \true, 'S' => \true, 'T' => \true, 'U' => \true, 'V' => \true, 'W' => \true, 'X' => \true, 'Y' => \true, 'Z' => \true, 'a' => \true, 'b' => \true, 'c' => \true, 'd' => \true, 'e' => \true, 'f' => \true, 'g' => \true, 'h' => \true, 'i' => \true, 'j' => \true, 'k' => \true, 'l' => \true, 'm' => \true, 'n' => \true, 'o' => \true, 'p' => \true, 'q' => \true, 'r' => \true, 's' => \true, 't' => \true, 'u' => \true, 'v' => \true, 'w' => \true, 'x' => \true, 'y' => \true, 'z' => \true, '_' => \true, '0' => \true, '1' => \true, '2' => \true, '3' => \true, '4' => \true, '5' => \true, '6' => \true, '7' => \true, '8' => \true, '9' => \true]; |
| 53 |
/** @var array Valid number characters after the first character */ |
| 54 |
private $numbers = ['0' => \true, '1' => \true, '2' => \true, '3' => \true, '4' => \true, '5' => \true, '6' => \true, '7' => \true, '8' => \true, '9' => \true]; |
| 55 |
/** @var array Map of simple single character tokens */ |
| 56 |
private $simpleTokens = ['.' => self::T_DOT, '*' => self::T_STAR, ']' => self::T_RBRACKET, ',' => self::T_COMMA, ':' => self::T_COLON, '@' => self::T_CURRENT, '(' => self::T_LPAREN, ')' => self::T_RPAREN, '{' => self::T_LBRACE, '}' => self::T_RBRACE]; |
| 57 |
/** |
| 58 |
* Tokenize the JMESPath expression into an array of tokens hashes that |
| 59 |
* contain a 'type', 'value', and 'key'. |
| 60 |
* |
| 61 |
* @param string $input JMESPath input |
| 62 |
* |
| 63 |
* @return array |
| 64 |
* @throws SyntaxErrorException |
| 65 |
*/ |
| 66 |
public function tokenize($input) |
| 67 |
{ |
| 68 |
$tokens = []; |
| 69 |
if ($input === '') { |
| 70 |
goto eof; |
| 71 |
} |
| 72 |
$chars = \str_split($input); |
| 73 |
while (\false !== ($current = \current($chars))) { |
| 74 |
// Every character must be in the transition character table. |
| 75 |
if (!isset(self::$transitionTable[$current])) { |
| 76 |
$tokens[] = ['type' => self::T_UNKNOWN, 'pos' => \key($chars), 'value' => $current]; |
| 77 |
\next($chars); |
| 78 |
continue; |
| 79 |
} |
| 80 |
$state = self::$transitionTable[$current]; |
| 81 |
if ($state === self::STATE_SINGLE_CHAR) { |
| 82 |
// Consume simple tokens like ".", ",", "@", etc. |
| 83 |
$tokens[] = ['type' => $this->simpleTokens[$current], 'pos' => \key($chars), 'value' => $current]; |
| 84 |
\next($chars); |
| 85 |
} elseif ($state === self::STATE_IDENTIFIER) { |
| 86 |
// Consume identifiers |
| 87 |
$start = \key($chars); |
| 88 |
$buffer = ''; |
| 89 |
do { |
| 90 |
$buffer .= $current; |
| 91 |
$current = \next($chars); |
| 92 |
} while ($current !== \false && isset($this->validIdentifier[$current])); |
| 93 |
$tokens[] = ['type' => self::T_IDENTIFIER, 'value' => $buffer, 'pos' => $start]; |
| 94 |
} elseif ($state === self::STATE_WHITESPACE) { |
| 95 |
// Skip whitespace |
| 96 |
\next($chars); |
| 97 |
} elseif ($state === self::STATE_LBRACKET) { |
| 98 |
// Consume "[", "[?", and "[]" |
| 99 |
$position = \key($chars); |
| 100 |
$actual = \next($chars); |
| 101 |
if ($actual === ']') { |
| 102 |
\next($chars); |
| 103 |
$tokens[] = ['type' => self::T_FLATTEN, 'pos' => $position, 'value' => '[]']; |
| 104 |
} elseif ($actual === '?') { |
| 105 |
\next($chars); |
| 106 |
$tokens[] = ['type' => self::T_FILTER, 'pos' => $position, 'value' => '[?']; |
| 107 |
} else { |
| 108 |
$tokens[] = ['type' => self::T_LBRACKET, 'pos' => $position, 'value' => '[']; |
| 109 |
} |
| 110 |
} elseif ($state === self::STATE_STRING_LITERAL) { |
| 111 |
// Consume raw string literals |
| 112 |
$t = $this->inside($chars, "'", self::T_LITERAL); |
| 113 |
$t['value'] = \str_replace("\\'", "'", $t['value']); |
| 114 |
$tokens[] = $t; |
| 115 |
} elseif ($state === self::STATE_PIPE) { |
| 116 |
// Consume pipe and OR |
| 117 |
$tokens[] = $this->matchOr($chars, '|', '|', self::T_OR, self::T_PIPE); |
| 118 |
} elseif ($state == self::STATE_JSON_LITERAL) { |
| 119 |
// Consume JSON literals |
| 120 |
$token = $this->inside($chars, '`', self::T_LITERAL); |
| 121 |
if ($token['type'] === self::T_LITERAL) { |
| 122 |
$token['value'] = \str_replace('\\`', '`', $token['value']); |
| 123 |
$token = $this->parseJson($token); |
| 124 |
} |
| 125 |
$tokens[] = $token; |
| 126 |
} elseif ($state == self::STATE_NUMBER) { |
| 127 |
// Consume numbers |
| 128 |
$start = \key($chars); |
| 129 |
$buffer = ''; |
| 130 |
do { |
| 131 |
$buffer .= $current; |
| 132 |
$current = \next($chars); |
| 133 |
} while ($current !== \false && isset($this->numbers[$current])); |
| 134 |
$tokens[] = ['type' => self::T_NUMBER, 'value' => (int) $buffer, 'pos' => $start]; |
| 135 |
} elseif ($state === self::STATE_QUOTED_STRING) { |
| 136 |
// Consume quoted identifiers |
| 137 |
$token = $this->inside($chars, '"', self::T_QUOTED_IDENTIFIER); |
| 138 |
if ($token['type'] === self::T_QUOTED_IDENTIFIER) { |
| 139 |
$token['value'] = '"' . $token['value'] . '"'; |
| 140 |
$token = $this->parseJson($token); |
| 141 |
} |
| 142 |
$tokens[] = $token; |
| 143 |
} elseif ($state === self::STATE_EQ) { |
| 144 |
// Consume equals |
| 145 |
$tokens[] = $this->matchOr($chars, '=', '=', self::T_COMPARATOR, self::T_UNKNOWN); |
| 146 |
} elseif ($state == self::STATE_AND) { |
| 147 |
$tokens[] = $this->matchOr($chars, '&', '&', self::T_AND, self::T_EXPREF); |
| 148 |
} elseif ($state === self::STATE_NOT) { |
| 149 |
// Consume not equal |
| 150 |
$tokens[] = $this->matchOr($chars, '!', '=', self::T_COMPARATOR, self::T_NOT); |
| 151 |
} else { |
| 152 |
// either '<' or '>' |
| 153 |
// Consume less than and greater than |
| 154 |
$tokens[] = $this->matchOr($chars, $current, '=', self::T_COMPARATOR, self::T_COMPARATOR); |
| 155 |
} |
| 156 |
} |
| 157 |
eof: |
| 158 |
$tokens[] = ['type' => self::T_EOF, 'pos' => \mb_strlen($input, 'UTF-8'), 'value' => null]; |
| 159 |
return $tokens; |
| 160 |
} |
| 161 |
/** |
| 162 |
* Returns a token based on whether or not the next token matches the |
| 163 |
* expected value. If it does, a token of "$type" is returned. Otherwise, |
| 164 |
* a token of "$orElse" type is returned. |
| 165 |
* |
| 166 |
* @param array $chars Array of characters by reference. |
| 167 |
* @param string $current The current character. |
| 168 |
* @param string $expected Expected character. |
| 169 |
* @param string $type Expected result type. |
| 170 |
* @param string $orElse Otherwise return a token of this type. |
| 171 |
* |
| 172 |
* @return array Returns a conditional token. |
| 173 |
*/ |
| 174 |
private function matchOr(array &$chars, $current, $expected, $type, $orElse) |
| 175 |
{ |
| 176 |
if (\next($chars) === $expected) { |
| 177 |
\next($chars); |
| 178 |
return ['type' => $type, 'pos' => \key($chars) - 1, 'value' => $current . $expected]; |
| 179 |
} |
| 180 |
return ['type' => $orElse, 'pos' => \key($chars) - 1, 'value' => $current]; |
| 181 |
} |
| 182 |
/** |
| 183 |
* Returns a token the is the result of consuming inside of delimiter |
| 184 |
* characters. Escaped delimiters will be adjusted before returning a |
| 185 |
* value. If the token is not closed, "unknown" is returned. |
| 186 |
* |
| 187 |
* @param array $chars Array of characters by reference. |
| 188 |
* @param string $delim The delimiter character. |
| 189 |
* @param string $type Token type. |
| 190 |
* |
| 191 |
* @return array Returns the consumed token. |
| 192 |
*/ |
| 193 |
private function inside(array &$chars, $delim, $type) |
| 194 |
{ |
| 195 |
$position = \key($chars); |
| 196 |
$current = \next($chars); |
| 197 |
$buffer = ''; |
| 198 |
while ($current !== $delim) { |
| 199 |
if ($current === '\\') { |
| 200 |
$buffer .= '\\'; |
| 201 |
$current = \next($chars); |
| 202 |
} |
| 203 |
if ($current === \false) { |
| 204 |
// Unclosed delimiter |
| 205 |
return ['type' => self::T_UNKNOWN, 'value' => $buffer, 'pos' => $position]; |
| 206 |
} |
| 207 |
$buffer .= $current; |
| 208 |
$current = \next($chars); |
| 209 |
} |
| 210 |
\next($chars); |
| 211 |
return ['type' => $type, 'value' => $buffer, 'pos' => $position]; |
| 212 |
} |
| 213 |
/** |
| 214 |
* Parses a JSON token or sets the token type to "unknown" on error. |
| 215 |
* |
| 216 |
* @param array $token Token that needs parsing. |
| 217 |
* |
| 218 |
* @return array Returns a token with a parsed value. |
| 219 |
*/ |
| 220 |
private function parseJson(array $token) |
| 221 |
{ |
| 222 |
$value = \json_decode($token['value'], \true); |
| 223 |
if ($error = \json_last_error()) { |
| 224 |
// Legacy support for elided quotes. Try to parse again by adding |
| 225 |
// quotes around the bad input value. |
| 226 |
$value = \json_decode('"' . $token['value'] . '"', \true); |
| 227 |
if ($error = \json_last_error()) { |
| 228 |
$token['type'] = self::T_UNKNOWN; |
| 229 |
return $token; |
| 230 |
} |
| 231 |
} |
| 232 |
$token['value'] = $value; |
| 233 |
return $token; |
| 234 |
} |
| 235 |
} |
| 236 |
|