Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  /**
   3   * CFPropertyList
   4   * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
   5   * @author Rodney Rehm <rodney.rehm@medialize.de>
   6   * @author Christian Kruse <cjk@wwwtech.de>
   7   * @package plist
   8   * @version $Id$
   9   */
  10  namespace CFPropertyList;
  11  
  12  /**
  13   * Facility for reading and writing binary PropertyLists. Ported from {@link http://www.opensource.apple.com/source/CF/CF-476.15/CFBinaryPList.c CFBinaryPList.c}.
  14   * @author Rodney Rehm <rodney.rehm@medialize.de>
  15   * @author Christian Kruse <cjk@wwwtech.de>
  16   * @package plist
  17   * @example example-read-02.php Read a Binary PropertyList
  18   * @example example-read-03.php Read a PropertyList without knowing the type
  19   */
  20  abstract class CFBinaryPropertyList {
  21    /**
  22     * Content of the plist (unparsed string)
  23     * @var string
  24     */
  25    protected $content = NULL;
  26  
  27    /**
  28     * position in the (unparsed) string
  29     * @var integer
  30     */
  31    protected $pos = 0;
  32  
  33    /**
  34     * Table containing uniqued objects
  35     * @var array
  36     */
  37    protected $uniqueTable = Array();
  38  
  39    /**
  40     * Number of objects in file
  41     * @var integer
  42     */
  43    protected $countObjects = 0;
  44  
  45    /**
  46     * The length of all strings in the file (byte length, not character length)
  47     * @var integer
  48     */
  49    protected $stringSize = 0;
  50  
  51    /**
  52     * The length of all ints in file (byte length)
  53     * @var integer
  54     */
  55    protected $intSize = 0;
  56  
  57    /**
  58     * The length of misc objects (i.e. not integer and not string) in file
  59     * @var integer
  60     */
  61    protected $miscSize = 0;
  62  
  63    /**
  64     * Number of object references in file (needed to calculate reference byte length)
  65     * @var integer
  66     */
  67    protected $objectRefs = 0;
  68  
  69    /**
  70     * Number of objects written during save phase; needed to calculate the size of the object table
  71     * @var integer
  72     */
  73    protected $writtenObjectCount = 0;
  74  
  75    /**
  76     * Table containing all objects in the file
  77     */
  78    protected $objectTable = Array();
  79  
  80    /**
  81     * The size of object references
  82     */
  83    protected $objectRefSize = 0;
  84  
  85    /**
  86     * The „offsets” (i.e. the different entries) in the file
  87     */
  88    protected $offsets = Array();
  89  
  90    /**
  91     * Read a „null type” (filler byte, true, false, 0 byte)
  92     * @param $length The byte itself
  93     * @return the byte value (e.g. CFBoolean(true), CFBoolean(false), 0 or 15)
  94     * @throws PListException on encountering an unknown null type
  95     */
  96    protected function readBinaryNullType($length) {
  97      switch($length) {
  98        case 0: return 0; // null type
  99        case 8: return new CFBoolean(false);
 100        case 9: return new CFBoolean(true);
 101        case 15: return 15; // fill type
 102      }
 103  
 104      throw new PListException("unknown null type: $length");
 105    }
 106  
 107    /**
 108     * Create an 64 bit integer using bcmath or gmp
 109     * @param int $hi The higher word
 110     * @param int $lo The lower word
 111     * @return mixed The integer (as int if possible, as string if not possible)
 112     * @throws PListException if neither gmp nor bc available
 113     */
 114    protected static function make64Int($hi,$lo) {
 115      // on x64, we can just use int
 116      if(PHP_INT_SIZE > 4) return (((int)$hi)<<32) | ((int)$lo);
 117  
 118      // lower word has to be unsigned since we don't use bitwise or, we use bcadd/gmp_add
 119      $lo = sprintf("%u", $lo);
 120  
 121      // use GMP or bcmath if possible
 122      if(function_exists("gmp_mul")) return gmp_strval(gmp_add(gmp_mul($hi, "4294967296"), $lo));
 123  
 124      if(function_exists("bcmul")) return bcadd(bcmul($hi,"4294967296"), $lo);
 125  
 126      if(class_exists('Math_BigInteger')) {
 127        $bi = new \Math_BigInteger($hi);
 128        return $bi->multiply(new \Math_BigInteger("4294967296"))->add(new \Math_BigInteger($lo))->toString();
 129      }
 130  
 131      throw new PListException("either gmp or bc has to be installed, or the Math_BigInteger has to be available!");
 132    }
 133  
 134    /**
 135     * Read an integer value
 136     * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
 137     * @return CFNumber The integer value
 138     * @throws PListException if integer val is invalid
 139     * @throws IOException if read error occurs
 140     * @uses make64Int() to overcome PHP's big integer problems
 141     */
 142    protected function readBinaryInt($length) {
 143      if($length > 3) throw new PListException("Integer greater than 8 bytes: $length");
 144  
 145      $nbytes = 1 << $length;
 146  
 147      $val = null;
 148      if(strlen($buff = substr($this->content, $this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
 149      $this->pos += $nbytes;
 150  
 151      switch($length) {
 152        case 0:
 153          $val = unpack("C", $buff);
 154          $val = $val[1];
 155          break;
 156        case 1:
 157          $val = unpack("n", $buff);
 158          $val = $val[1];
 159          break;
 160        case 2:
 161          $val = unpack("N", $buff);
 162          $val = $val[1];
 163          break;
 164        case 3:
 165          $words = unpack("Nhighword/Nlowword",$buff);
 166          //$val = $words['highword'] << 32 | $words['lowword'];
 167          $val = self::make64Int($words['highword'],$words['lowword']);
 168          break;
 169      }
 170  
 171      return new CFNumber($val);
 172    }
 173  
 174    /**
 175     * Read a real value
 176     * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
 177     * @return CFNumber The real value
 178     * @throws PListException if real val is invalid
 179     * @throws IOException if read error occurs
 180     */
 181    protected function readBinaryReal($length) {
 182      if($length > 3) throw new PListException("Real greater than 8 bytes: $length");
 183  
 184      $nbytes = 1 << $length;
 185      $val = null;
 186      if(strlen($buff = substr($this->content,$this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
 187      $this->pos += $nbytes;
 188  
 189      switch($length) {
 190        case 0: // 1 byte float? must be an error
 191        case 1: // 2 byte float? must be an error
 192          $x = $length + 1;
 193          throw new PListException("got {$x} byte float, must be an error!");
 194        case 2:
 195          $val = unpack("f", strrev($buff));
 196          $val = $val[1];
 197          break;
 198        case 3:
 199          $val = unpack("d", strrev($buff));
 200          $val = $val[1];
 201          break;
 202      }
 203  
 204      return new CFNumber($val);
 205    }
 206  
 207    /**
 208     * Read a date value
 209     * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
 210     * @return CFDate The date value
 211     * @throws PListException if date val is invalid
 212     * @throws IOException if read error occurs
 213     */
 214    protected function readBinaryDate($length) {
 215      if($length > 3) throw new PListException("Date greater than 8 bytes: $length");
 216  
 217      $nbytes = 1 << $length;
 218      $val = null;
 219      if(strlen($buff = substr($this->content, $this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
 220      $this->pos += $nbytes;
 221  
 222      switch($length) {
 223        case 0: // 1 byte CFDate is an error
 224        case 1: // 2 byte CFDate is an error
 225          $x = $length + 1;
 226          throw new PListException("{$x} byte CFdate, error");
 227  
 228        case 2:
 229          $val = unpack("f", strrev($buff));
 230          $val = $val[1];
 231          break;
 232        case 3:
 233          $val = unpack("d", strrev($buff));
 234          $val = $val[1];
 235          break;
 236      }
 237  
 238      return new CFDate($val,CFDate::TIMESTAMP_APPLE);
 239    }
 240  
 241    /**
 242     * Read a data value
 243     * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
 244     * @return CFData The data value
 245     * @throws IOException if read error occurs
 246     */
 247    protected function readBinaryData($length) {
 248      if($length == 0) $buff = "";
 249      else {
 250        $buff = substr($this->content, $this->pos, $length);
 251        if(strlen($buff) != $length) throw IOException::readError("");
 252        $this->pos += $length;
 253      }
 254  
 255      return new CFData($buff,false);
 256    }
 257  
 258    /**
 259     * Read a string value, usually coded as utf8
 260     * @param integer $length The length (in bytes) of the string value
 261     * @return CFString The string value, utf8 encoded
 262     * @throws IOException if read error occurs
 263     */
 264    protected function readBinaryString($length) {
 265      if($length == 0) $buff = "";
 266      else {
 267        if(strlen($buff = substr($this->content, $this->pos, $length)) != $length) throw IOException::readError("");
 268        $this->pos += $length;
 269      }
 270  
 271      if(!isset($this->uniqueTable[$buff])) $this->uniqueTable[$buff] = true;
 272      return new CFString($buff);
 273    }
 274  
 275    /**
 276     * Convert the given string from one charset to another.
 277     * Trying to use MBString, Iconv, Recode - in that particular order.
 278     * @param string $string the string to convert
 279     * @param string $fromCharset the charset the given string is currently encoded in
 280     * @param string $toCharset the charset to convert to, defaults to UTF-8
 281     * @return string the converted string
 282     * @throws PListException on neither MBString, Iconv, Recode being available
 283     */
 284    public static function convertCharset($string, $fromCharset, $toCharset='UTF-8') {
 285      if(function_exists('mb_convert_encoding')) return mb_convert_encoding($string, $toCharset, $fromCharset);
 286      if(function_exists('iconv')) return iconv($fromCharset, $toCharset, $string);
 287      if(function_exists('recode_string')) return recode_string($fromCharset .'..'. $toCharset, $string);
 288  
 289      throw new PListException('neither iconv nor mbstring supported. how are we supposed to work on strings here?');
 290    }
 291  
 292    /**
 293     * Count characters considering character set
 294     * Trying to use MBString, Iconv - in that particular order.
 295     * @param string $string the string to convert
 296     * @param string $charset the charset the given string is currently encoded in
 297     * @return integer The number of characters in that string
 298     * @throws PListException on neither MBString, Iconv being available
 299     */
 300    public static function charsetStrlen($string,$charset="UTF-8") {
 301      if(function_exists('mb_strlen')) return mb_strlen($string, $charset);
 302      if(function_exists('iconv_strlen')) return iconv_strlen($string,$charset);
 303  
 304      throw new PListException('neither iconv nor mbstring supported. how are we supposed to work on strings here?');
 305    }
 306  
 307    /**
 308     * Read a unicode string value, coded as UTF-16BE
 309     * @param integer $length The length (in bytes) of the string value
 310     * @return CFString The string value, utf8 encoded
 311     * @throws IOException if read error occurs
 312     */
 313    protected function readBinaryUnicodeString($length) {
 314      /* The problem is: we get the length of the string IN CHARACTERS;
 315         since a char in UTF-16 can be 16 or 32 bit long, we don't really know
 316         how long the string is in bytes */
 317      if(strlen($buff = substr($this->content, $this->pos, 2*$length)) != 2*$length) throw IOException::readError("");
 318      $this->pos += 2 * $length;
 319  
 320      if(!isset($this->uniqueTable[$buff])) $this->uniqueTable[$buff] = true;
 321      return new CFString(self::convertCharset($buff, "UTF-16BE", "UTF-8"));
 322    }
 323  
 324    /**
 325     * Read an array value, including contained objects
 326     * @param integer $length The number of contained objects
 327     * @return CFArray The array value, including the objects
 328     * @throws IOException if read error occurs
 329     */
 330    protected function readBinaryArray($length) {
 331      $ary = new CFArray();
 332  
 333      // first: read object refs
 334      if($length != 0) {
 335        if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
 336        $this->pos += $length * $this->objectRefSize;
 337  
 338        $objects = self::unpackWithSize($this->objectRefSize, $buff);
 339  
 340        // now: read objects
 341        for($i=0;$i<$length;++$i) {
 342          $object = $this->readBinaryObjectAt($objects[$i+1]+1);
 343          $ary->add($object);
 344        }
 345      }
 346  
 347      return $ary;
 348    }
 349  
 350    /**
 351     * Read a dictionary value, including contained objects
 352     * @param integer $length The number of contained objects
 353     * @return CFDictionary The dictionary value, including the objects
 354     * @throws IOException if read error occurs
 355     */
 356    protected function readBinaryDict($length) {
 357      $dict = new CFDictionary();
 358  
 359      // first: read keys
 360      if($length != 0) {
 361        if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
 362        $this->pos += $length * $this->objectRefSize;
 363        $keys = self::unpackWithSize($this->objectRefSize, $buff);
 364  
 365        // second: read object refs
 366        if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
 367        $this->pos += $length * $this->objectRefSize;
 368        $objects = self::unpackWithSize($this->objectRefSize, $buff);
 369  
 370        // read real keys and objects
 371        for($i=0;$i<$length;++$i) {
 372          $key = $this->readBinaryObjectAt($keys[$i+1]+1);
 373          $object = $this->readBinaryObjectAt($objects[$i+1]+1);
 374          $dict->add($key->getValue(),$object);
 375        }
 376      }
 377  
 378      return $dict;
 379    }
 380  
 381    /**
 382     * Read an object type byte, decode it and delegate to the correct reader function
 383     * @return mixed The value of the delegate reader, so any of the CFType subclasses
 384     * @throws IOException if read error occurs
 385     */
 386    function readBinaryObject() {
 387      // first: read the marker byte
 388      if(strlen($buff = substr($this->content,$this->pos,1)) != 1) throw IOException::readError("");
 389      $this->pos++;
 390  
 391      $object_length = unpack("C*", $buff);
 392      $object_length = $object_length[1]  & 0xF;
 393      $buff = unpack("H*", $buff);
 394      $buff = $buff[1];
 395  
 396      $object_type = substr($buff, 0, 1);
 397      if($object_type != "0" && $object_length == 15) {
 398        $object_length = $this->readBinaryObject($this->objectRefSize);
 399        $object_length = $object_length->getValue();
 400      }
 401  
 402      $retval = null;
 403      switch($object_type) {
 404        case '0': // null, false, true, fillbyte
 405          $retval = $this->readBinaryNullType($object_length);
 406          break;
 407        case '1': // integer
 408          $retval = $this->readBinaryInt($object_length);
 409          break;
 410        case '2': // real
 411          $retval = $this->readBinaryReal($object_length);
 412          break;
 413        case '3': // date
 414          $retval = $this->readBinaryDate($object_length);
 415          break;
 416        case '4': // data
 417          $retval = $this->readBinaryData($object_length);
 418          break;
 419        case '5': // byte string, usually utf8 encoded
 420          $retval = $this->readBinaryString($object_length);
 421          break;
 422        case '6': // unicode string (utf16be)
 423          $retval = $this->readBinaryUnicodeString($object_length);
 424          break;
 425        case '8':
 426          $num = $this->readBinaryInt($object_length);
 427          $retval = new CFUid($num->getValue());
 428          break;
 429        case 'a': // array
 430          $retval = $this->readBinaryArray($object_length);
 431          break;
 432        case 'd': // dictionary
 433          $retval = $this->readBinaryDict($object_length);
 434          break;
 435      }
 436  
 437      return $retval;
 438    }
 439  
 440    /**
 441     * Read an object type byte at position $pos, decode it and delegate to the correct reader function
 442     * @param integer $pos The table position in the offsets table
 443     * @return mixed The value of the delegate reader, so any of the CFType subclasses
 444     */
 445    function readBinaryObjectAt($pos) {
 446      $this->pos = $this->offsets[$pos];
 447      return $this->readBinaryObject();
 448    }
 449  
 450    /**
 451     * Parse a binary plist string
 452     * @return void
 453     * @throws IOException if read error occurs
 454     */
 455    public function parseBinaryString() {
 456      $this->uniqueTable = Array();
 457      $this->countObjects = 0;
 458      $this->stringSize = 0;
 459      $this->intSize = 0;
 460      $this->miscSize = 0;
 461      $this->objectRefs = 0;
 462  
 463      $this->writtenObjectCount = 0;
 464      $this->objectTable = Array();
 465      $this->objectRefSize = 0;
 466  
 467      $this->offsets = Array();
 468  
 469      // first, we read the trailer: 32 byte from the end
 470      $buff = substr($this->content,-32);
 471  
 472      if(strlen($buff) < 32) {
 473        throw new PListException('Error in PList format: content is less than at least necessary 32 bytes!');
 474      }
 475  
 476      $infos = unpack("x6/Coffset_size/Cobject_ref_size/x4/Nnumber_of_objects/x4/Ntop_object/x4/Ntable_offset",$buff);
 477  
 478      // after that, get the offset table
 479      $coded_offset_table = substr($this->content,$infos['table_offset'],$infos['number_of_objects'] * $infos['offset_size']);
 480      if(strlen($coded_offset_table) != $infos['number_of_objects'] * $infos['offset_size']) throw IOException::readError("");
 481      $this->countObjects = $infos['number_of_objects'];
 482  
 483      // decode offset table
 484      $formats = Array("","C*","n*",NULL,"N*");
 485      if($infos['offset_size'] == 3) { # since PHP does not support parenthesis in pack/unpack expressions,
 486                                       # "(H6)*" does not work and we have to work round this by repeating the
 487                                       # expression as often as it fits in the string
 488        $this->offsets = array(NULL);
 489        while($coded_offset_table) {
 490          $str = unpack("H6",$coded_offset_table);
 491          $this->offsets[] = hexdec($str[1]);
 492          $coded_offset_table = substr($coded_offset_table,3);
 493        }
 494      }
 495      else $this->offsets = unpack($formats[$infos['offset_size']],$coded_offset_table);
 496  
 497      $this->uniqueTable = Array();
 498      $this->objectRefSize = $infos['object_ref_size'];
 499  
 500      $top = $this->readBinaryObjectAt($infos['top_object']+1);
 501      $this->add($top);
 502    }
 503  
 504    /**
 505     * Read a binary plist stream
 506     * @param resource $stream The stream to read
 507     * @return void
 508     * @throws IOException if read error occurs
 509     */
 510    function readBinaryStream($stream) {
 511      if(($str = stream_get_contents($stream)) === false || empty($str)) {
 512        throw new PListException("Error reading stream!");
 513      }
 514  
 515      $this->parseBinary($str);
 516    }
 517  
 518    /**
 519     * parse a binary plist string
 520     * @param string $content The stream to read, defaults to {@link $this->content}
 521     * @return void
 522     * @throws IOException if read error occurs
 523     */
 524    function parseBinary($content=NULL) {
 525      if($content !== NULL) {
 526        $this->content = $content;
 527      }
 528  
 529      if(empty($this->content)) {
 530        throw new PListException("Content may not be empty!");
 531      }
 532  
 533      if(substr($this->content,0,8) != 'bplist00') {
 534        throw new PListException("Invalid binary string!");
 535      }
 536  
 537      $this->pos = 0;
 538  
 539      $this->parseBinaryString();
 540    }
 541  
 542    /**
 543     * Read a binary plist file
 544     * @param string $file The file to read
 545     * @return void
 546     * @throws IOException if read error occurs
 547     */
 548    function readBinary($file) {
 549      if(!($fd = fopen($file,"rb"))) {
 550        throw new IOException("Could not open file {$file}!");
 551      }
 552  
 553      $this->readBinaryStream($fd);
 554      fclose($fd);
 555    }
 556  
 557    /**
 558     * calculate the bytes needed for a size integer value
 559     * @param integer $int The integer value to calculate
 560     * @return integer The number of bytes needed
 561     */
 562    public static function bytesSizeInt($int) {
 563      $nbytes = 0;
 564  
 565      if($int > 0xE) $nbytes += 2; // 2 size-bytes
 566      if($int > 0xFF) $nbytes += 1; // 3 size-bytes
 567      if($int > 0xFFFF) $nbytes += 2; // 5 size-bytes
 568  
 569      return $nbytes;
 570    }
 571  
 572    /**
 573     * Calculate the byte needed for a „normal” integer value
 574     * @param integer $int The integer value
 575     * @return integer The number of bytes needed + 1 (because of the „marker byte”)
 576     */
 577    public static function bytesInt($int) {
 578      $nbytes = 1;
 579  
 580      if($int > 0xFF) $nbytes += 1; // 2 byte integer
 581      if($int > 0xFFFF) $nbytes += 2; // 4 byte integer
 582      if($int > 0xFFFFFFFF) $nbytes += 4; // 8 byte integer
 583      if($int < 0) $nbytes += 7; // 8 byte integer (since it is signed)
 584  
 585      return $nbytes + 1; // one „marker” byte
 586    }
 587  
 588    /**
 589     * „pack” a value (i.e. write the binary representation as big endian to a string) with the specified size
 590     * @param integer $nbytes The number of bytes to pack
 591     * @param integer $int The integer value to pack
 592     * @return string The packed value as string
 593     */
 594    public static function packItWithSize($nbytes,$int) {
 595      $formats = Array("C", "n", "N", "N");
 596      $format = $formats[$nbytes-1];
 597  
 598      if($nbytes == 3) return substr(pack($format, $int), -3);
 599      return pack($format, $int);
 600    }
 601  
 602    /**
 603     * „unpack” multiple values of the specified size (i.e. get the integers from their binary representation) from a string
 604     * @param integer $nbytes The number of bytes of each value to unpack
 605     * @param integer $buff The string packed with integer values
 606     * @return array The unpacked integers
 607     */
 608    public static function unpackWithSize($nbytes,$buff) {
 609      $formats = Array("C*", "n*", "N*", "N*");
 610      $format = $formats[$nbytes-1];
 611  
 612      if($nbytes == 3) $buff = "\0" . implode("\0", str_split($buff, 3));
 613      return unpack($format, $buff);
 614    }
 615  
 616    /**
 617     * Calculate the bytes needed to save the number of objects
 618     * @param integer $count_objects The number of objects
 619     * @return integer The number of bytes
 620     */
 621    public static function bytesNeeded($count_objects) {
 622      $nbytes = 0;
 623  
 624      while($count_objects >= 1) {
 625        $nbytes++;
 626        $count_objects /= 256;
 627      }
 628  
 629      return $nbytes;
 630    }
 631  
 632    /**
 633     * Code an integer to byte representation
 634     * @param integer $int The integer value
 635     * @return string The packed byte value
 636     */
 637    public static function intBytes($int) {
 638      $intbytes = "";
 639  
 640      if($int > 0xFFFF) $intbytes = "\x12".pack("N", $int); // 4 byte integer
 641      elseif($int > 0xFF) $intbytes = "\x11".pack("n", $int); // 2 byte integer
 642      else $intbytes = "\x10".pack("C", $int); // 8 byte integer
 643  
 644      return $intbytes;
 645    }
 646  
 647    /**
 648     * Code an type byte, consisting of the type marker and the length of the type
 649     * @param string $type The type byte value (i.e. "d" for dictionaries)
 650     * @param integer $type_len The length of the type
 651     * @return string The packed type byte value
 652     */
 653    public static function typeBytes($type,$type_len) {
 654      $optional_int = "";
 655  
 656      if($type_len < 15) $type .= sprintf("%x", $type_len);
 657      else {
 658        $type .= "f";
 659        $optional_int = self::intBytes($type_len);
 660      }
 661  
 662      return pack("H*", $type).$optional_int;
 663    }
 664  
 665    /**
 666     * Count number of objects and create a unique table for strings
 667     * @param $value The value to count and unique
 668     * @return void
 669     */
 670    protected function uniqueAndCountValues($value) {
 671      // no uniquing for other types than CFString and CFData
 672      if($value instanceof CFNumber) {
 673        $val = $value->getValue();
 674        if(intval($val) == $val && !is_float($val) && strpos($val,'.') === false) $this->intSize += self::bytesInt($val);
 675        else $this->miscSize += 9; // 9 bytes (8 + marker byte) for real
 676        $this->countObjects++;
 677        return;
 678      }
 679      elseif($value instanceof CFDate) {
 680        $this->miscSize += 9; // since date in plist is real, we need 9 byte (8 + marker byte)
 681        $this->countObjects++;
 682        return;
 683      }
 684      elseif($value instanceof CFBoolean) {
 685        $this->countObjects++;
 686        $this->miscSize += 1;
 687        return;
 688      }
 689      elseif($value instanceof CFArray) {
 690        $cnt = 0;
 691        foreach($value as $v) {
 692          ++$cnt;
 693          $this->uniqueAndCountValues($v);
 694          $this->objectRefs++; // each array member is a ref
 695        }
 696  
 697        $this->countObjects++;
 698        $this->intSize += self::bytesSizeInt($cnt);
 699        $this->miscSize++; // marker byte for array
 700        return;
 701      }
 702      elseif($value instanceof CFDictionary) {
 703        $cnt = 0;
 704        foreach($value as $k => $v) {
 705          ++$cnt;
 706          if(!isset($this->uniqueTable[$k])) {
 707            $this->uniqueTable[$k] = 0;
 708            $len = self::binaryStrlen($k);
 709            $this->stringSize += $len + 1;
 710            $this->intSize += self::bytesSizeInt(self::charsetStrlen($k,'UTF-8'));
 711          }
 712  
 713          $this->objectRefs += 2; // both, key and value, are refs
 714          $this->uniqueTable[$k]++;
 715          $this->uniqueAndCountValues($v);
 716        }
 717  
 718        $this->countObjects++;
 719        $this->miscSize++; // marker byte for dict
 720        $this->intSize += self::bytesSizeInt($cnt);
 721        return;
 722      }
 723      elseif($value instanceOf CFData) {
 724        $val = $value->getValue();
 725        $len = strlen($val);
 726        $this->intSize += self::bytesSizeInt($len);
 727        $this->miscSize += $len + 1;
 728        $this->countObjects++;
 729        return;
 730      }
 731      else $val = $value->getValue();
 732  
 733      if(!isset($this->uniqueTable[$val])) {
 734        $this->uniqueTable[$val] = 0;
 735        $len = self::binaryStrlen($val);
 736        $this->stringSize += $len + 1;
 737        $this->intSize += self::bytesSizeInt(self::charsetStrlen($val,'UTF-8'));
 738      }
 739      $this->uniqueTable[$val]++;
 740    }
 741  
 742    /**
 743     * Convert CFPropertyList to binary format; since we have to count our objects we simply unique CFDictionary and CFArray
 744     * @return string The binary plist content
 745     */
 746    public function toBinary() {
 747      $this->uniqueTable = Array();
 748      $this->countObjects = 0;
 749      $this->stringSize = 0;
 750      $this->intSize = 0;
 751      $this->miscSize = 0;
 752      $this->objectRefs = 0;
 753  
 754      $this->writtenObjectCount = 0;
 755      $this->objectTable = Array();
 756      $this->objectRefSize = 0;
 757  
 758      $this->offsets = Array();
 759  
 760      $binary_str = "bplist00";
 761      $value = $this->getValue(true);
 762      $this->uniqueAndCountValues($value);
 763  
 764      $this->countObjects += count($this->uniqueTable);
 765      $this->objectRefSize = self::bytesNeeded($this->countObjects);
 766      $file_size = $this->stringSize + $this->intSize + $this->miscSize + $this->objectRefs * $this->objectRefSize + 40;
 767      $offset_size = self::bytesNeeded($file_size);
 768      $table_offset = $file_size - 32;
 769  
 770      $this->objectTable = Array();
 771      $this->writtenObjectCount = 0;
 772      $this->uniqueTable = Array(); // we needed it to calculate several values
 773      $value->toBinary($this);
 774  
 775      $object_offset = 8;
 776      $offsets = Array();
 777  
 778      for($i=0;$i<count($this->objectTable);++$i) {
 779        $binary_str .= $this->objectTable[$i];
 780        $offsets[$i] = $object_offset;
 781        $object_offset += strlen($this->objectTable[$i]);
 782      }
 783  
 784      for($i=0;$i<count($offsets);++$i) {
 785        $binary_str .= self::packItWithSize($offset_size, $offsets[$i]);
 786      }
 787  
 788  
 789      $binary_str .= pack("x6CC", $offset_size, $this->objectRefSize);
 790      $binary_str .= pack("x4N", $this->countObjects);
 791      $binary_str .= pack("x4N", 0);
 792      $binary_str .= pack("x4N", $table_offset);
 793  
 794      return $binary_str;
 795    }
 796  
 797    /**
 798     * Counts the number of bytes the string will have when coded; utf-16be if non-ascii characters are present.
 799     * @param string $val The string value
 800     * @return integer The length of the coded string in bytes
 801     */
 802    protected static function binaryStrlen($val) {
 803      for($i=0;$i<strlen($val);++$i) {
 804        if(ord($val[$i]) >= 128) {
 805          $val = self::convertCharset($val, 'UTF-8', 'UTF-16BE');
 806          return strlen($val);
 807        }
 808      }
 809  
 810      return strlen($val);
 811    }
 812  
 813    /**
 814     * Uniques and transforms a string value to binary format and adds it to the object table
 815     * @param string $val The string value
 816     * @return integer The position in the object table
 817     */
 818    public function stringToBinary($val) {
 819      $saved_object_count = -1;
 820  
 821      if(!isset($this->uniqueTable[$val])) {
 822        $saved_object_count = $this->writtenObjectCount++;
 823        $this->uniqueTable[$val] = $saved_object_count;
 824        $utf16 = false;
 825  
 826        for($i=0;$i<strlen($val);++$i) {
 827          if(ord($val[$i]) >= 128) {
 828            $utf16 = true;
 829            break;
 830          }
 831        }
 832  
 833        if($utf16) {
 834          $bdata = self::typeBytes("6", mb_strlen($val,'UTF-8')); // 6 is 0110, unicode string (utf16be)
 835          $val = self::convertCharset($val, 'UTF-8', 'UTF-16BE');
 836          $this->objectTable[$saved_object_count] = $bdata.$val;
 837        }
 838        else {
 839          $bdata = self::typeBytes("5", strlen($val)); // 5 is 0101 which is an ASCII string (seems to be ASCII encoded)
 840          $this->objectTable[$saved_object_count] = $bdata.$val;
 841        }
 842      }
 843      else $saved_object_count = $this->uniqueTable[$val];
 844  
 845      return $saved_object_count;
 846    }
 847  
 848    /**
 849     * Codes an integer to binary format
 850     * @param integer $value The integer value
 851     * @return string the coded integer
 852     */
 853    protected function intToBinary($value) {
 854      $nbytes = 0;
 855      if($value > 0xFF) $nbytes = 1; // 1 byte integer
 856      if($value > 0xFFFF) $nbytes += 1; // 4 byte integer
 857      if($value > 0xFFFFFFFF) $nbytes += 1; // 8 byte integer
 858      if($value < 0) $nbytes = 3; // 8 byte integer, since signed
 859  
 860      $bdata = self::typeBytes("1", $nbytes); // 1 is 0001, type indicator for integer
 861      $buff = "";
 862  
 863      if($nbytes < 3) {
 864        if($nbytes == 0) $fmt = "C";
 865        elseif($nbytes == 1) $fmt = "n";
 866        else $fmt = "N";
 867  
 868        $buff = pack($fmt, $value);
 869      }
 870      else {
 871        if(PHP_INT_SIZE > 4) {
 872          // 64 bit signed integer; we need the higher and the lower 32 bit of the value
 873          $high_word = $value >> 32;
 874          $low_word = $value & 0xFFFFFFFF;
 875        }
 876        else {
 877          // since PHP can only handle 32bit signed, we can only get 32bit signed values at this point - values above 0x7FFFFFFF are
 878          // floats. So we ignore the existance of 64bit on non-64bit-machines
 879          if($value < 0) $high_word = 0xFFFFFFFF;
 880          else $high_word = 0;
 881          $low_word = $value;
 882        }
 883        $buff = pack("N", $high_word).pack("N", $low_word);
 884      }
 885  
 886      return $bdata.$buff;
 887    }
 888  
 889    /**
 890     * Codes a real value to binary format
 891     * @param float $val The real value
 892     * @return string The coded real
 893     */
 894    protected function realToBinary($val) {
 895      $bdata = self::typeBytes("2", 3); // 2 is 0010, type indicator for reals
 896      return $bdata.strrev(pack("d", (float)$val));
 897    }
 898  
 899    public function uidToBinary($value) {
 900      $saved_object_count = $this->writtenObjectCount++;
 901  
 902      $val = "";
 903  
 904      $nbytes = 0;
 905      if($value > 0xFF) $nbytes = 1; // 1 byte integer
 906      if($value > 0xFFFF) $nbytes += 1; // 4 byte integer
 907      if($value > 0xFFFFFFFF) $nbytes += 1; // 8 byte integer
 908      if($value < 0) $nbytes = 3; // 8 byte integer, since signed
 909  
 910      $bdata = self::typeBytes("1000", $nbytes); // 1 is 0001, type indicator for integer
 911      $buff = "";
 912  
 913      if($nbytes < 3) {
 914        if($nbytes == 0) $fmt = "C";
 915        elseif($nbytes == 1) $fmt = "n";
 916        else $fmt = "N";
 917  
 918        $buff = pack($fmt, $value);
 919      }
 920      else {
 921        if(PHP_INT_SIZE > 4) {
 922          // 64 bit signed integer; we need the higher and the lower 32 bit of the value
 923          $high_word = $value >> 32;
 924          $low_word = $value & 0xFFFFFFFF;
 925        }
 926        else {
 927          // since PHP can only handle 32bit signed, we can only get 32bit signed values at this point - values above 0x7FFFFFFF are
 928          // floats. So we ignore the existance of 64bit on non-64bit-machines
 929          if($value < 0) $high_word = 0xFFFFFFFF;
 930          else $high_word = 0;
 931          $low_word = $value;
 932        }
 933        $buff = pack("N", $high_word).pack("N", $low_word);
 934      }
 935  
 936      $val = $bdata.$buff;
 937  
 938      $this->objectTable[$saved_object_count] = $val;
 939      return $saved_object_count;
 940    }
 941  
 942    /**
 943     * Converts a numeric value to binary and adds it to the object table
 944     * @param numeric $value The numeric value
 945     * @return integer The position in the object table
 946     */
 947    public function numToBinary($value) {
 948      $saved_object_count = $this->writtenObjectCount++;
 949  
 950      $val = "";
 951      if(intval($value) == $value && !is_float($value) && strpos($value,'.') === false) $val = $this->intToBinary($value);
 952      else $val = $this->realToBinary($value);
 953  
 954      $this->objectTable[$saved_object_count] = $val;
 955      return $saved_object_count;
 956    }
 957  
 958    /**
 959     * Convert date value (apple format) to binary and adds it to the object table
 960     * @param integer $value The date value
 961     * @return integer The position of the coded value in the object table
 962     */
 963    public function dateToBinary($val) {
 964      $saved_object_count = $this->writtenObjectCount++;
 965  
 966      $hour = gmdate("H",$val);
 967      $min = gmdate("i",$val);
 968      $sec = gmdate("s",$val);
 969      $mday = gmdate("j",$val);
 970      $mon = gmdate("n",$val);
 971      $year = gmdate("Y",$val);
 972  
 973      $val = gmmktime($hour,$min,$sec,$mon,$mday,$year) - CFDate::DATE_DIFF_APPLE_UNIX; // CFDate is a real, number of seconds since 01/01/2001 00:00:00 GMT
 974  
 975      $bdata = self::typeBytes("3", 3); // 3 is 0011, type indicator for date
 976      $this->objectTable[$saved_object_count] = $bdata.strrev(pack("d", $val));
 977  
 978      return $saved_object_count;
 979    }
 980  
 981    /**
 982     * Convert a bool value to binary and add it to the object table
 983     * @param bool $val The boolean value
 984     * @return integer The position in the object table
 985     */
 986    public function boolToBinary($val) {
 987      $saved_object_count = $this->writtenObjectCount++;
 988      $this->objectTable[$saved_object_count] = $val ? "\x9" : "\x8"; // 0x9 is 1001, type indicator for true; 0x8 is 1000, type indicator for false
 989      return $saved_object_count;
 990    }
 991  
 992    /**
 993     * Convert data value to binary format and add it to the object table
 994     * @param string $val The data value
 995     * @return integer The position in the object table
 996     */
 997    public function dataToBinary($val) {
 998      $saved_object_count = $this->writtenObjectCount++;
 999  
1000      $bdata = self::typeBytes("4", strlen($val)); // a is 1000, type indicator for data
1001      $this->objectTable[$saved_object_count] = $bdata.$val;
1002  
1003      return $saved_object_count;
1004    }
1005  
1006    /**
1007     * Convert array to binary format and add it to the object table
1008     * @param CFArray $val The array to convert
1009     * @return integer The position in the object table
1010     */
1011    public function arrayToBinary($val) {
1012      $saved_object_count = $this->writtenObjectCount++;
1013  
1014      $bdata = self::typeBytes("a", count($val->getValue())); // a is 1010, type indicator for arrays
1015  
1016      foreach($val as $v) {
1017        $bval = $v->toBinary($this);
1018        $bdata .= self::packItWithSize($this->objectRefSize, $bval);
1019      }
1020  
1021      $this->objectTable[$saved_object_count] = $bdata;
1022      return $saved_object_count;
1023    }
1024  
1025    /**
1026     * Convert dictionary to binary format and add it to the object table
1027     * @param CFDictionary $val The dict to convert
1028     * @return integer The position in the object table
1029     */
1030    public function dictToBinary($val) {
1031      $saved_object_count = $this->writtenObjectCount++;
1032      $bdata = self::typeBytes("d", count($val->getValue())); // d=1101, type indicator for dictionary
1033  
1034      foreach($val as $k => $v) {
1035        $str = new CFString($k);
1036        $key = $str->toBinary($this);
1037        $bdata .= self::packItWithSize($this->objectRefSize, $key);
1038      }
1039  
1040      foreach($val as $k => $v) {
1041        $bval = $v->toBinary($this);
1042        $bdata .= self::packItWithSize($this->objectRefSize, $bval);
1043      }
1044  
1045      $this->objectTable[$saved_object_count] = $bdata;
1046      return $saved_object_count;
1047    }
1048  
1049  }
1050  
1051  # eof