Skip to content

Instantly share code, notes, and snippets.

@axiixc
Created February 27, 2011 02:53
Show Gist options
  • Save axiixc/845863 to your computer and use it in GitHub Desktop.
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.
<?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