PluginProbe
Packeta / 1.2.6
Packeta v1.2.6
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / packetery_vendor / nette / neon / src / Neon / Decoder.php

Decoder.php in Packeta 1.2.6, at packetery_vendor/nette/neon/src/Neon/Decoder.php

397 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is part of the PacketeryNette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7
8 declare(strict_types=1);
9
10 namespace PacketeryNette\Neon;
11
12
13 /**
14 * Parser for PacketeryNette Object Notation.
15 * @internal
16 */
17 final class Decoder
18 {
19 public const PATTERNS = [
20 // strings
21 '
22 \'\'\'\n (?:(?: [^\n] | \n(?![\t\ ]*+\'\'\') )*+ \n)?[\t\ ]*+\'\'\' |
23 """\n (?:(?: [^\n] | \n(?![\t\ ]*+""") )*+ \n)?[\t\ ]*+""" |
24 \' (?: \'\' | [^\'\n] )*+ \' |
25 " (?: \\\\. | [^"\\\\\n] )*+ "
26 ',
27
28 // literal / boolean / integer / float
29 '
30 (?: [^#"\',:=[\]{}()\n\t\ `-] | (?<!["\']) [:-] [^"\',=[\]{}()\n\t\ ] )
31 (?:
32 [^,:=\]})(\n\t\ ]++ |
33 :(?! [\n\t\ ,\]})] | $ ) |
34 [\ \t]++ [^#,:=\]})(\n\t\ ]
35 )*+
36 ',
37
38 // punctuation
39 '[,:=[\]{}()-]',
40
41 // comment
42 '?:\#.*+',
43
44 // new line + indent
45 '\n[\t\ ]*+',
46
47 // whitespace
48 '?:[\t\ ]++',
49 ];
50
51 private const PATTERN_DATETIME = '#\d\d\d\d-\d\d?-\d\d?(?:(?:[Tt]| ++)\d\d?:\d\d:\d\d(?:\.\d*+)? *+(?:Z|[-+]\d\d?(?::?\d\d)?)?)?$#DA';
52
53 private const PATTERN_HEX = '#0x[0-9a-fA-F]++$#DA';
54
55 private const PATTERN_OCTAL = '#0o[0-7]++$#DA';
56
57 private const PATTERN_BINARY = '#0b[0-1]++$#DA';
58
59 private const SIMPLE_TYPES = [
60 'true' => 'TRUE', 'True' => 'TRUE', 'TRUE' => 'TRUE', 'yes' => 'TRUE', 'Yes' => 'TRUE', 'YES' => 'TRUE', 'on' => 'TRUE', 'On' => 'TRUE', 'ON' => 'TRUE',
61 'false' => 'FALSE', 'False' => 'FALSE', 'FALSE' => 'FALSE', 'no' => 'FALSE', 'No' => 'FALSE', 'NO' => 'FALSE', 'off' => 'FALSE', 'Off' => 'FALSE', 'OFF' => 'FALSE',
62 'null' => 'NULL', 'Null' => 'NULL', 'NULL' => 'NULL',
63 ];
64
65 private const DEPRECATED_TYPES = ['on' => 1, 'On' => 1, 'ON' => 1, 'off' => 1, 'Off' => 1, 'OFF' => 1];
66
67 private const ESCAPE_SEQUENCES = [
68 't' => "\t", 'n' => "\n", 'r' => "\r", 'f' => "\x0C", 'b' => "\x08", '"' => '"', '\\' => '\\', '/' => '/', '_' => "\u{A0}",
69 ];
70
71 private const BRACKETS = [
72 '[' => ']',
73 '{' => '}',
74 '(' => ')',
75 ];
76
77 /** @var string */
78 private $input;
79
80 /** @var array */
81 private $tokens;
82
83 /** @var int */
84 private $pos;
85
86
87 /**
88 * Decodes a NEON string.
89 * @return mixed
90 */
91 public function decode(string $input)
92 {
93 if (substr($input, 0, 3) === "\u{FEFF}") { // BOM
94 $input = substr($input, 3);
95 }
96 $this->input = "\n" . str_replace("\r", '', $input); // \n forces indent detection
97
98 $pattern = '~(' . implode(')|(', self::PATTERNS) . ')~Amixu';
99 $this->tokens = preg_split($pattern, $this->input, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_DELIM_CAPTURE);
100 if ($this->tokens === false) {
101 throw new Exception('Invalid UTF-8 sequence.');
102 }
103
104 $last = end($this->tokens);
105 if ($this->tokens && !preg_match($pattern, $last[0])) {
106 $this->pos = count($this->tokens) - 1;
107 $this->error();
108 }
109
110 $this->pos = 0;
111 $res = $this->parse(null);
112
113 while (isset($this->tokens[$this->pos])) {
114 if ($this->tokens[$this->pos][0][0] === "\n") {
115 $this->pos++;
116 } else {
117 $this->error();
118 }
119 }
120 return $res;
121 }
122
123
124 /**
125 * @param string|bool|null $indent indentation (for block-parser)
126 * @return mixed
127 */
128 private function parse($indent, array $result = null, $key = null, bool $hasKey = false)
129 {
130 $inlineParser = $indent === false;
131 $value = null;
132 $hasValue = false;
133 $tokens = $this->tokens;
134 $n = &$this->pos;
135 $count = count($tokens);
136 $mainResult = &$result;
137
138 for (; $n < $count; $n++) {
139 $t = $tokens[$n][0];
140
141 if ($t === ',') { // ArrayEntry separator
142 if ((!$hasKey && !$hasValue) || !$inlineParser) {
143 $this->error();
144 }
145 $this->addValue($result, $hasKey ? $key : null, $hasValue ? $value : null);
146 $hasKey = $hasValue = false;
147
148 } elseif ($t === ':' || $t === '=') { // KeyValuePair separator
149 if ($hasValue && (is_array($value) || is_object($value))) {
150 $this->error('Unacceptable key');
151
152 } elseif ($hasKey && $key === null && $hasValue && !$inlineParser) {
153 $n++;
154 $result[] = $this->parse($indent . ' ', [], $value, true);
155 $newIndent = isset($tokens[$n], $tokens[$n + 1]) // not last
156 ? (string) substr($tokens[$n][0], 1)
157 : '';
158 if (strlen($newIndent) > strlen($indent)) {
159 $n++;
160 $this->error('Bad indentation');
161 } elseif (strlen($newIndent) < strlen($indent)) {
162 return $mainResult; // block parser exit point
163 }
164 $hasKey = $hasValue = false;
165
166 } elseif ($hasKey || !$hasValue) {
167 $this->error();
168
169 } else {
170 $key = (string) $value;
171 $hasKey = true;
172 $hasValue = false;
173 $result = &$mainResult;
174 }
175
176 } elseif ($t === '-') { // BlockArray bullet
177 if ($hasKey || $hasValue || $inlineParser) {
178 $this->error();
179 }
180 $key = null;
181 $hasKey = true;
182
183 } elseif (isset(self::BRACKETS[$t])) { // Opening bracket [ ( {
184 if ($hasValue) {
185 if ($t !== '(') {
186 $this->error();
187 }
188 $n++;
189 if ($value instanceof Entity && $value->value === Neon::CHAIN) {
190 end($value->attributes)->attributes = $this->parse(false, []);
191 } else {
192 $value = new Entity($value, $this->parse(false, []));
193 }
194 } else {
195 $n++;
196 $value = $this->parse(false, []);
197 }
198 $hasValue = true;
199 if ( // unexpected type of bracket or block-parser
200 !isset($tokens[$n])
201 || $tokens[$n][0] !== self::BRACKETS[$t]
202 ) {
203 $this->error();
204 }
205
206 } elseif ($t === ']' || $t === '}' || $t === ')') { // Closing bracket ] ) }
207 if (!$inlineParser) {
208 $this->error();
209 }
210 break;
211
212 } elseif ($t[0] === "\n") { // Indent
213 if ($inlineParser) {
214 if ($hasKey || $hasValue) {
215 $this->addValue($result, $hasKey ? $key : null, $hasValue ? $value : null);
216 $hasKey = $hasValue = false;
217 }
218
219 } else {
220 while (isset($tokens[$n + 1]) && $tokens[$n + 1][0][0] === "\n") {
221 $n++; // skip to last indent
222 }
223 if (!isset($tokens[$n + 1])) {
224 break;
225 }
226
227 $newIndent = (string) substr($tokens[$n][0], 1);
228 if ($indent === null) { // first iteration
229 $indent = $newIndent;
230 }
231 $minlen = min(strlen($newIndent), strlen($indent));
232 if ($minlen && (string) substr($newIndent, 0, $minlen) !== (string) substr($indent, 0, $minlen)) {
233 $n++;
234 $this->error('Invalid combination of tabs and spaces');
235 }
236
237 if (strlen($newIndent) > strlen($indent)) { // open new block-array or hash
238 if ($hasValue || !$hasKey) {
239 $n++;
240 $this->error('Bad indentation');
241 }
242 $this->addValue($result, $key, $this->parse($newIndent));
243 $newIndent = isset($tokens[$n], $tokens[$n + 1]) // not last
244 ? (string) substr($tokens[$n][0], 1)
245 : '';
246 if (strlen($newIndent) > strlen($indent)) {
247 $n++;
248 $this->error('Bad indentation');
249 }
250 $hasKey = false;
251
252 } else {
253 if ($hasValue && !$hasKey) { // block items must have "key"; null key means list item
254 break;
255
256 } elseif ($hasKey) {
257 $this->addValue($result, $key, $hasValue ? $value : null);
258 if (
259 $key !== null
260 && !$hasValue
261 && $newIndent === $indent
262 && isset($tokens[$n + 1])
263 && $tokens[$n + 1][0] === '-'
264 ) {
265 $result = &$result[$key];
266 }
267 $hasKey = $hasValue = false;
268 }
269 }
270
271 if (strlen($newIndent) < strlen($indent)) { // close block
272 return $mainResult; // block parser exit point
273 }
274 }
275
276 } else { // Value
277 $isKey = ($tmp = $tokens[$n + 1][0] ?? null) && ($tmp === ':' || $tmp === '=');
278
279 if ($t[0] === '"' || $t[0] === "'") {
280 if (preg_match('#^...\n++([\t ]*+)#', $t, $m)) {
281 $converted = substr($t, 3, -3);
282 $converted = str_replace("\n" . $m[1], "\n", $converted);
283 $converted = preg_replace('#^\n|\n[\t ]*+$#D', '', $converted);
284 } else {
285 $converted = substr($t, 1, -1);
286 if ($t[0] === "'") {
287 $converted = str_replace("''", "'", $converted);
288 }
289 }
290 if ($t[0] === '"') {
291 $converted = preg_replace_callback('#\\\\(?:ud[89ab][0-9a-f]{2}\\\\ud[c-f][0-9a-f]{2}|u[0-9a-f]{4}|x[0-9a-f]{2}|.)#i', [$this, 'cbString'], $converted);
292 }
293 } elseif (!$isKey && isset(self::SIMPLE_TYPES[$t])) {
294 $converted = constant(self::SIMPLE_TYPES[$t]);
295 if (isset(self::DEPRECATED_TYPES[$t])) {
296 trigger_error("Neon: keyword '$t' is deprecated, use true/yes or false/no.", E_USER_DEPRECATED);
297 }
298 } elseif (is_numeric($t)) {
299 $converted = $t * 1;
300 } elseif (preg_match(self::PATTERN_HEX, $t)) {
301 $converted = hexdec($t);
302 } elseif (preg_match(self::PATTERN_OCTAL, $t)) {
303 $converted = octdec($t);
304 } elseif (preg_match(self::PATTERN_BINARY, $t)) {
305 $converted = bindec($t);
306 } elseif (!$isKey && preg_match(self::PATTERN_DATETIME, $t)) {
307 $converted = new \DateTimeImmutable($t);
308 } else { // literal
309 $converted = $t;
310 }
311 if ($hasValue) {
312 if ($value instanceof Entity) { // Entity chaining
313 if ($value->value !== Neon::CHAIN) {
314 $value = new Entity(Neon::CHAIN, [$value]);
315 }
316 $value->attributes[] = new Entity($converted);
317 } else {
318 $this->error();
319 }
320 } else {
321 $value = $converted;
322 $hasValue = true;
323 }
324 }
325 }
326
327 if ($inlineParser) {
328 if ($hasKey || $hasValue) {
329 $this->addValue($result, $hasKey ? $key : null, $hasValue ? $value : null);
330 }
331 } else {
332 if ($hasValue && !$hasKey) { // block items must have "key"
333 if ($result === null) {
334 $result = $value; // simple value parser
335 } else {
336 $this->error();
337 }
338 } elseif ($hasKey) {
339 $this->addValue($result, $key, $hasValue ? $value : null);
340 }
341 }
342 return $mainResult;
343 }
344
345
346 private function addValue(&$result, $key, $value)
347 {
348 if ($key === null) {
349 $result[] = $value;
350 } elseif ($result && array_key_exists($key, $result)) {
351 $this->error("Duplicated key '$key'");
352 } else {
353 $result[$key] = $value;
354 }
355 }
356
357
358 private function cbString(array $m): string
359 {
360 $sq = $m[0];
361 if (isset(self::ESCAPE_SEQUENCES[$sq[1]])) {
362 return self::ESCAPE_SEQUENCES[$sq[1]];
363 } elseif ($sq[1] === 'u' && strlen($sq) >= 6) {
364 $lead = hexdec(substr($sq, 2, 4));
365 $tail = hexdec(substr($sq, 8, 4));
366 $code = $tail ? (0x2400 + (($lead - 0xD800) << 10) + $tail) : $lead;
367 if ($code >= 0xD800 && $code <= 0xDFFF) {
368 $this->error("Invalid UTF-8 (lone surrogate) $sq");
369 }
370 return function_exists('iconv')
371 ? iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code))
372 : mb_convert_encoding(pack('N', $code), 'UTF-8', 'UTF-32BE');
373
374 } elseif ($sq[1] === 'x' && strlen($sq) === 4) {
375 trigger_error("Neon: '$sq' is deprecated, use '\\uXXXX' instead.", E_USER_DEPRECATED);
376 return chr(hexdec(substr($sq, 2)));
377
378 } else {
379 $this->error("Invalid escaping sequence $sq");
380 }
381 }
382
383
384 private function error(string $message = "Unexpected '%s'")
385 {
386 $last = $this->tokens[$this->pos] ?? null;
387 $offset = $last ? $last[1] : strlen($this->input);
388 $text = substr($this->input, 0, $offset);
389 $line = substr_count($text, "\n");
390 $col = $offset - strrpos("\n" . $text, "\n") + 1;
391 $token = $last
392 ? str_replace("\n", '<new line>', substr($last[0], 0, 40))
393 : 'end';
394 throw new Exception(str_replace('%s', $token, $message) . " on line $line, column $col.");
395 }
396 }
397