|
<?php |
|
|
|
// |
|
// OpenStep Property List Parser |
|
// |
|
// Copyright 2024 Florian Pircher |
|
// |
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
|
// you may not use this file except in compliance with the License. |
|
// You may obtain a copy of the License at |
|
// |
|
// https://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// Unless required by applicable law or agreed to in writing, software |
|
// distributed under the License is distributed on an "AS IS" BASIS, |
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
// See the License for the specific language governing permissions and |
|
// limitations under the License. |
|
// |
|
|
|
class OpenStepPlistParser { |
|
private $data; |
|
private $index; |
|
private $length; |
|
|
|
static function parse($data) { |
|
$parser = new OpenStepPlistParser($data); |
|
$value = $parser->parseValue(); |
|
|
|
if ($parser->head() !== null) { |
|
throw new Exception('UnexpectedTrailingCharacters'); |
|
} |
|
|
|
return $value; |
|
} |
|
|
|
function __construct($data) { |
|
$this->data = $data; |
|
$this->index = 0; |
|
$this->length = strlen($data); |
|
} |
|
|
|
function advance($predicate) { |
|
while ($this->index < $this->length && $predicate($this->data[$this->index])) { |
|
$this->index++; |
|
} |
|
} |
|
|
|
function head() { |
|
return $this->index < $this->length ? $this->data[$this->index] : null; |
|
} |
|
|
|
function next() { |
|
if ($this->index < $this->length) { |
|
$char = $this->data[$this->index]; |
|
$this->index++; |
|
return $char; |
|
} |
|
else { |
|
return null; |
|
} |
|
} |
|
|
|
function read($char) { |
|
if ($this->index < $this->length && $this->data[$this->index] === $char) { |
|
$this->index++; |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
function skipTrivia() { |
|
$this->advance(function($char) { |
|
return $char === ' ' || $char === "\n" || $char === "\t" || $char === "\r"; |
|
}); |
|
} |
|
|
|
function parseValue() { |
|
$this->skipTrivia(); |
|
|
|
$head = $this->head(); |
|
|
|
if ($head === null) { |
|
return null; |
|
} |
|
|
|
if ($head === '(') { |
|
$this->index++; |
|
|
|
$array = []; |
|
|
|
$this->skipTrivia(); |
|
|
|
while (($value = $this->parseValue()) !== null) { |
|
$array[] = $value; |
|
|
|
$this->skipTrivia(); |
|
|
|
if ($this->read(',')) { |
|
$this->skipTrivia(); |
|
} |
|
else { |
|
break; |
|
} |
|
} |
|
|
|
if (!$this->read(')')) { |
|
throw new Exception('MissingClosingParenthesis'); |
|
} |
|
|
|
$this->skipTrivia(); |
|
|
|
return $array; |
|
} |
|
else if ($head === '{') { |
|
$this->index++; |
|
|
|
$dictionary = []; |
|
|
|
$this->skipTrivia(); |
|
|
|
while (($key = $this->parseValue()) !== null) { |
|
if (!is_string($key)) { |
|
throw new Exception('NonStringKey'); |
|
} |
|
|
|
$this->skipTrivia(); |
|
|
|
if (!$this->read('=')) { |
|
throw new Exception('MissingEqualSignInDictionary'); |
|
} |
|
|
|
$this->skipTrivia(); |
|
|
|
if (($value = $this->parseValue()) === null) { |
|
throw new Exception('MissingValueInDictionary'); |
|
} |
|
|
|
$this->skipTrivia(); |
|
|
|
if (!$this->read(';')) { |
|
throw new Exception('MissingSemicolonInDictionary'); |
|
} |
|
|
|
$this->skipTrivia(); |
|
|
|
$dictionary[$key] = $value; |
|
} |
|
|
|
if (!$this->read('}')) { |
|
throw new Exception('MissingClosingBrace'); |
|
} |
|
|
|
$this->skipTrivia(); |
|
|
|
return $dictionary; |
|
} |
|
else if ($head === '"' || $head === "'") { |
|
$this->index++; |
|
|
|
$buffer = ''; |
|
|
|
while (true) { |
|
while (($char = $this->head()) !== null && $char !== $head && $char !== '\\') { |
|
$buffer .= $char; |
|
$this->index++; |
|
} |
|
|
|
$stopChar = $this->next(); |
|
|
|
if ($stopChar === $head) { |
|
break; |
|
} |
|
else if ($stopChar === '\\') { |
|
if ($this->index >= $this->length) { |
|
throw new Exception('MissingClosingQuote'); |
|
} |
|
|
|
$specialChar = $this->next(); |
|
|
|
switch ($specialChar) { |
|
case '\\': |
|
$buffer .= '\\'; |
|
break; |
|
case '"': |
|
$buffer .= '"'; |
|
break; |
|
case "'": |
|
$buffer .= "'"; |
|
break; |
|
case 'a': |
|
$buffer .= "\x07"; |
|
break; |
|
case 'b': |
|
$buffer .= "\x08"; |
|
break; |
|
case 'e': |
|
$buffer .= "\x1B"; |
|
break; |
|
case 'f': |
|
$buffer .= "\x0C"; |
|
break; |
|
case 'n': |
|
$buffer .= "\n"; |
|
break; |
|
case 'r': |
|
$buffer .= "\r"; |
|
break; |
|
case 't': |
|
$buffer .= "\t"; |
|
break; |
|
case 'v': |
|
$buffer .= "\x0B"; |
|
break; |
|
case '?': |
|
$buffer .= '?'; |
|
break; |
|
case "\n": |
|
$buffer .= "\n"; |
|
break; |
|
case '0': |
|
case '1': |
|
case '2': |
|
case '3': |
|
case '4': |
|
case '5': |
|
case '6': |
|
case '7': |
|
$n1 = $specialChar; |
|
if (($n2 = $this->next()) !== null && ($n3 = $this->next()) !== null && $n2 >= '0' && $n2 <= '7' && $n3 >= '0' && $n3 <= '7') { |
|
$octalCode = $n1 . $n2 . $n3; |
|
$codepoint = octdec($octalCode); |
|
$buffer .= chr($codepoint); |
|
} |
|
else { |
|
throw new Exception('InvalidOctalEscapeSequence'); |
|
} |
|
break; |
|
default: |
|
throw new Exception('UnknownSpecialCharacter'); |
|
} |
|
} |
|
else { |
|
throw new Exception('UnexpectedEndOfString'); |
|
} |
|
} |
|
|
|
return $buffer; |
|
} |
|
else if ($head === '$' || $head === '+' || ($head >= '-' && $head <= '9') || ($head >= 'A' && $head <= 'Z') || $head === '_' || ($head >= 'a' && $head <= 'z')) { |
|
$string = ''; |
|
|
|
while (($char = $this->head()) !== null && ($char === '$' || $char === '+' || ($char >= '-' && $char <= '9') || ($char >= 'A' && $char <= 'Z') || $char === '_' || ($char >= 'a' && $char <= 'z'))) { |
|
$string .= $char; |
|
$this->index++; |
|
} |
|
|
|
return $string; |
|
} |
|
else { |
|
return null; |
|
} |
|
} |
|
} |