CFBinaryPropertyList.php 36 KB

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