Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 39 and 311]

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