See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401]
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
title
Description
Body
title
Description
Body
title
Description
Body
title
Body