Created
February 27, 2011 02:53
-
-
Save axiixc/845863 to your computer and use it in GitHub Desktop.
Convert php data structures to binary pllst. Utilities to simulate Core Foundations types also provided.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
# axiixc [ 2011 ] - https://gist.github.com/845863 | |
# Adapted from a ruby version at https://gist.github.com/303378 | |
# Usage | |
$data = CFDictMake( | |
"False", FALSE, | |
"True", TRUE, | |
"Integer", 42, | |
"Float", 3.14, | |
"String", "The quick brown fox", | |
"High Bit String", "∆x ∂y ß´∑†å", | |
"Data '0x01'", new CFData("\x01"), | |
"Date", new CFDate(time()), | |
"Array", CFArrayMake(1, 2, 3) | |
); | |
$file = fopen('Data.plist', 'w'); | |
fwrite($file, AppleBinaryPropertyList::convert($data)); | |
fclose($file); | |
# Implementation | |
define('CFCast', 'CFCast'); | |
define('CFCastArray', 'CFCastArray'); | |
define('CFCastDict', 'CFCastDict'); | |
date_default_timezone_set('America/New_York'); | |
function CFArrayMake() { | |
$seed = func_get_args(); | |
$seed[CFCast] = CFCastArray; | |
return $seed; | |
} | |
function CFArrayCast(&$array) { | |
$array[CFCast] = CFCastArray; | |
} | |
function CFDictMake() { | |
$s = func_get_args(); | |
$seed = array(); | |
for ($i = 0; $i < count($s); $i += 2) | |
$seed[$s[$i]] = $s[$i+1]; | |
$seed[CFCast] = CFCastDict; | |
return $seed; | |
} | |
function CFDictCast(&$dict) { | |
$dict[CFCast] = CFCastDict; | |
} | |
class CFData { | |
public $data; | |
public function __construct($data = NULL) { $this->set($data); } | |
public function set($data) { $this->data = NULL; $this->append($data); } | |
public function append($data) { $this->data .= pack('d*', $data); } | |
} | |
class CFDate { | |
const FormatPLIST = 'Y-m-d\TH:i:s\Z'; | |
const DateEpochOffsetAppleUnix = 978307200; | |
public $date; | |
public function __construct($date) { $this->date = $date; } | |
public function description() { date(self::FormatPLIST, strtotime($date)); } | |
public function timeIntervalSince1970() { return $this->date; } | |
public function timeIntervalSinceReferenceDate() { return $this->date - self::DateEpochOffsetAppleUnix; } | |
} | |
class AppleBinaryPropertyList { | |
const MIME_TYPE = 'application/octet-stream'; | |
# Text encoding | |
const INPUT_TEXT_ENCODING = 'UTF-8'; | |
const PLIST_TEXT_ENCODING = 'UTF-16BE'; | |
static function convert($data) { | |
$ret = ""; | |
return self::write($ret, $data); | |
return $ret; | |
} | |
static function write(&$out, $data) { | |
# Find out how many objects there are, so we know how big the references are | |
$count = self::countObjects($data); | |
list($ref_format, $ref_size) = self::intFormatAndSize($count); | |
# Now serialize all the objects | |
$values = array(); | |
self::appendValues($data, $values, $ref_format); | |
# Write header, then the values, calculating offsets as they're written | |
$out .= 'bplist00'; | |
$offset = 8; | |
$offsets = array(); | |
foreach ($values as $v) { | |
$offsets[] = $offset; | |
$out .= $v; | |
$offset += is_array($v) ? count($v) : strlen($v) ; | |
} | |
# How big should the offset ints be? | |
# Decoder gets upset if the size can't fit the entire file, even if it's not strictly needed, so add the length of the last value. | |
$last_value = $values[count($offsets) + 1]; | |
$last_value_count = is_array($last_value) ? count($last_value) : strlen($last_value) ; | |
list($offset_format, $offset_size) = self::intFormatAndSize($offsets[count($offsets) - 1] + $last_value_count); | |
# Write the offsets | |
foreach ($offsets as $o) | |
$out .= pack($offset_format, $o); | |
# Write the trailer | |
$out .= pack('NnCCNNNNNN', 0, 0, $offset_size, $ref_size, 0, count($values), 0, 0, 0, $offset); | |
return $out; | |
} | |
// Private (ish) | |
static function countObjects($data) { | |
if (is_array($data)) { | |
$type = $data[CFCast]; | |
unset($data[CFCast]); | |
$sum = ($type == CFCastDict) ? count($data->elements) : 1 ; | |
foreach ($data as $value) | |
$sum += self::countObjects($value); | |
return $sum; | |
} | |
return 1; | |
} | |
static function appendValues($data, &$values, $ref_format) { | |
if ($data === NULL) { | |
# return $values[] .= "\x00"; | |
throw new Exception('Can\'t store a nil in a binary plist. While the format supports it, decoders don\'t like it.'); | |
} | |
else if ($data === FALSE) { | |
return $values[] = "\x08"; | |
} | |
else if ($data === TRUE) { | |
return $values[] = "\x09"; | |
} | |
else if (is_int($data)) { | |
if ($data < -2147483648 || $data > 0x7FFFFFFF) | |
throw new Exception('Integer out of range to write in binary plist'); | |
return $values[] = self::packedInt($data); | |
} | |
else if (is_float($data)) { | |
return $values[] .= "\x23" . strrev(pack('d*', $data)); | |
} | |
else if (is_string($data)) { | |
if (preg_match('/[\x80-\xff]/', ~$data)) { | |
# Has high bits set, so is UTF-8 and must be reencoded for the plist file | |
$c = iconv(self::INPUT_TEXT_ENCODING, self::PLIST_TEXT_ENCODING,$data); | |
$o = self::objhdrWithLength(0x60, strlen($c) / 2); | |
$o .= $c; | |
return $values[] = $o; | |
} | |
else { | |
# Just ASCII | |
$o = self::objhdrWithLength(0x50, strlen($data)); | |
$o .= $data; | |
return $values[] = $o; | |
} | |
} | |
else if (is_object($data) && strtolower(get_class($data)) == 'cfdata') { | |
$o = self::objhdrWithLength(0x40, strlen($data->data)); | |
$o .= $data->data; | |
return $values[] = $o; | |
} | |
else if (is_object($data) && strtolower(get_class($data)) == 'cfdate') { | |
return $values[] = "\x33" . strrev(pack('d*', $data->timeIntervalSinceReferenceDate())); | |
} | |
else if (is_array($data) && $data[CFCast] == CFCastDict) { | |
unset($data[CFCast]); | |
$o = self::objhdrWithLength(0xa0, count($data)); | |
$values[] = $o; | |
$vIndex = count($values) - 1; | |
$refs = array(); | |
foreach ($data as $e) { | |
$refs[] = count($values); | |
self::appendValues($e, $values, $ref_format); | |
} | |
foreach ($refs as $REF) | |
$values[$vIndex] .= pack($ref_format, $REF); | |
return $o; | |
} | |
else if (is_array($data)) { | |
# Assume type dictionary | |
unset($data[CFCast]); | |
$o = self::objhdrWithLength(0xd0, count($data)); | |
$values[] = $o; | |
$vIndex = count($values) - 1; | |
$ks = array(); | |
$vs = array(); | |
foreach ($data as $k => $v) { | |
$ks[] = count($values); | |
self::appendValues($k, $values, $ref_format); | |
$vs[] = count($values); | |
self::appendValues($v, $values, $ref_format); | |
} | |
foreach ($ks as $KS) | |
$values[$vIndex] .= pack($ref_format, $KS); | |
foreach ($vs as $VS) | |
$values[$vIndex] .= pack($ref_format, $VS); | |
return $o; | |
} | |
else { | |
throw new Exception('Couldn\'t serialize value of data'); | |
} | |
} | |
static function intFormatAndSize($int) | |
{ | |
if ($int > 0xffff) | |
return array('N*', 4); | |
else if ($int > 0xff) | |
return array('n*', 2); | |
else | |
return array('c*', 1); | |
} | |
static function packedInt($data) | |
{ | |
if ($data < 0) | |
# Needs to be 64 bits for negative numbers | |
return pack('CNN', 0x13, 0xffffffff, $data); | |
else if ($data > 0xffff) | |
return pack('CN', 0x12, $data); | |
else if ($data > 0xff) | |
return pack('Cn', 0x11, $data); | |
else | |
return pack('CC', 0x10, $data); | |
} | |
static function objhdrWithLength($id, $length) | |
{ | |
if ($length < 0xf) | |
return chr($id + $length); | |
else | |
return chr($id + 0xf) . self::packedInt($length); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment