Skip to content

Instantly share code, notes, and snippets.

@florianpircher
Last active May 23, 2024 11:31
Show Gist options
  • Save florianpircher/987afc466447cb7fe67893e148b80557 to your computer and use it in GitHub Desktop.
Save florianpircher/987afc466447cb7fe67893e148b80557 to your computer and use it in GitHub Desktop.

PHP OpenStep Property List Parser

This code parses an OpenStep-flavor (aka. NeXTSTEP, ASCII, old-style) property list to a PHP value (string, array, or associative array). Useful for parsing general property list files, and Glyphs files (.glyphs, .glyph) in particular.

Usage

$data = file_get_contents("some-file.plist");

try {
    $value = OpenStepPlistParser::parse($data);
    // - string, if top level is "..." or '...' or unquoted string
    // - array, if top level is ( ... )
    // - associative array, if top level is { ... }
    var_dump($value);
}
catch (Exception $e) {
    // handle exception
}
<?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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment