1<?php 2/** 3 * PHP_Archive Manager Class creator (allows debugging/manipulation of phar files) 4 * 5 * @package PHP_Archive 6 * @category PHP 7 */ 8/** 9 * Needed for file manipulation 10 */ 11require_once 'System.php'; 12require_once 'PHP/Archive/Exception.php'; 13 14 15/** 16 * 17 * @copyright Copyright ? Gregory Beaver 18 * @author Greg Beaver <cellog@php.net> 19 * @version $Id$ 20 * @package PHP_Archive 21 * @category PHP 22 */ 23class PHP_Archive_Manager 24{ 25 const GZ = 0x00001000; 26 const BZ2 = 0x00002000; 27 const SIG = 0x00010000; 28 const SHA1 = 0x0002; 29 const MD5 = 0x0001; 30 private $_alias; 31 private $_archiveName; 32 private $_apiVersion; 33 private $_flags; 34 private $_knownAPIVersions = array('1.0.0', '1.1.0'); 35 private $_manifest; 36 private $_fileStart; 37 private $_manifestSize; 38 private $_html; 39 private $_metadata; 40 private $_sigtype; 41 private $_signature = false; 42 /** 43 * Locate the .phar archive in the include_path and detect the file to open within 44 * the archive. 45 * 46 * Possible parameters are phar://filename_within_phar.ext or 47 * phar://pharname.phar/filename_within_phar.ext 48 * 49 * phar://filename_within_phar.ext will simply use the last .phar opened. 50 * @param string a file within the archive 51 * @return string the filename within the .phar to retrieve 52 */ 53 public function __construct($phar) 54 { 55 $this->_archiveName = $phar; 56 $this->validate(); 57 } 58 59 /** 60 * validate a phar prior to manipulating it 61 * @throws PHP_Archive_Exception 62 */ 63 public function validate($strict = false) 64 { 65 $errors = array(); 66 $warnings = array(); 67 $fp = fopen($this->_archiveName, 'rb'); 68 if (!$fp) { 69 throw new PHP_Archive_ExceptionExtended(PHP_Archive_ExceptionExtended::NOOPEN, 70 array('archive' => $this->_archiveName)); 71 } 72 $header = fread($fp, strlen('<?php #PHP_ARCHIVE_HEADER-')); 73 if ($header == '<?php #PHP_ARCHIVE_HEADER-') { 74 $version = ''; 75 while (!feof($fp) && (false !== $c = fgetc($fp))) { 76 if ((ord($c) < ord('0') || ord($c) > ord('9')) && $c != '.') { 77 break; 78 } 79 $version .= $c; 80 } 81 if (version_compare($version, '0.8.0', '<')) { 82 throw new PHP_Archive_Exception($phar . ' was created with obsolete PHP_Archive', 83 $errors); 84 } 85 $this->_version = $version; 86 } 87 // seek to __HALT_COMPILER_OFFSET__ 88 $found = false; 89 while (!feof($fp) && false != ($next = fread($fp, 8192))) { 90 if (false != ($t = strpos($next, '__HALT_COMPILER();'))) { 91 fseek($fp, $t - strlen($next) + strlen('__HALT_COMPILER();'), SEEK_CUR); 92 $found = true; 93 break; 94 } 95 } 96 if (!$found) { 97 throw new PHP_Archive_ExceptionExtended(PHP_Archive_ExceptionExtended::NOTPHAR, 98 array('archive' => $this->_archiveName)); 99 } 100 $manifest_length = fread($fp, 4); 101 $manifest_length = unpack('Vlen', $manifest_length); 102 $this->_manifestSize = $manifest_length = $manifest_length['len']; 103 if ($manifest_length > 1048576) { 104 if ($strict) { 105 throw new PHP_Archive_ExceptionExtended( 106 PHP_Archive_ExceptionExtended::MANIFESTOVERFLOW, array( 107 'archive' => $this->_archiveName)); 108 } 109 $errors[] = new PHP_Archive_ExceptionExtended( 110 PHP_Archive_ExceptionExtended::MANIFESTOVERFLOW, array( 111 'archive' => $this->_archiveName)); 112 } 113 $manifest = fread($fp, $manifest_length); 114 // retrieve the number of files in the manifest 115 $info = unpack('V', substr($manifest, 0, 4)); 116 if ($info[1] * 24 > $manifest_length) { 117 $errors[] = new PHP_Archive_ExceptionExtended( 118 PHP_Archive_ExceptionExtended::MANIFESTENTRIESOVERFLOW,array( 119 'archive' => $this->_archiveName)); 120 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 121 } 122 $manifest = substr($manifest, 4); 123 if (strlen($manifest) < 4) { 124 $errors[] = new PHP_Archive_ExceptionExtended( 125 PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array( 126 'archive' => $this->_archiveName)); 127 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 128 } 129 // get API version and compressed flag 130 $apiver = substr($manifest, 0, 2); 131 $apiver = bin2hex($apiver); 132 $this->_apiVersion = hexdec($apiver[0]) . '.' . hexdec($apiver[1]) . 133 '.' . hexdec($apiver[2]); 134 if (!in_array($this->_apiVersion, $this->_knownAPIVersions)) { 135 $errors[] = new PHP_Archive_ExceptionExtended( 136 PHP_Archive_ExceptionExtended::UNKNOWNAPI, array( 137 'archive' => $this->_archiveName, 'ver' => $this->_apiVersion)); 138 throw new PHP_Archive_Exception('phar "' . $this->_archiveName . '" cannot be analyzed', $errors); 139 } 140 $manifest = substr($manifest, 2); 141 if (strlen($manifest) < 4) { 142 $errors[] = new PHP_Archive_ExceptionExtended( 143 PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array( 144 'archive' => $this->_archiveName)); 145 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 146 } 147 // get flags 148 $flags = unpack('V', substr($manifest, 0, 4)); 149 $this->_flags = $flags[1]; 150 $manifest = substr($manifest, 4); 151 if (strlen($manifest) < 4) { 152 $errors[] = new PHP_Archive_ExceptionExtended( 153 PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array( 154 'archive' => $this->_archiveName)); 155 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 156 } 157 // get alias 158 $aliaslen = unpack('V', substr($manifest, 0, 4)); 159 $aliaslen = $aliaslen[1]; 160 $manifest = substr($manifest, 4); 161 if (strlen($manifest) < $aliaslen) { 162 $errors[] = new PHP_Archive_ExceptionExtended( 163 PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array( 164 'archive' => $this->_archiveName)); 165 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 166 } 167 $this->_alias = substr($manifest, 0, $aliaslen); 168 $manifest = substr($manifest, $aliaslen); 169 // phar metadata 170 if (strlen($manifest) < 4) { 171 $errors[] = new PHP_Archive_ExceptionExtended( 172 PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array( 173 'archive' => $this->_archiveName)); 174 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 175 } 176 $metadatalen = unpack('V', substr($manifest, 0, 4)); 177 $metadatalen = $metadatalen[1]; 178 $manifest = substr($manifest, 4); 179 if ($metadatalen) { 180 if (strlen($manifest) < $metadatalen) { 181 $errors[] = new PHP_Archive_ExceptionExtended( 182 PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array( 183 'archive' => $this->_archiveName)); 184 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 185 } 186 $this->_metadata = unserialize(substr($manifest, 0, $metadatalen)); 187 $manifest = substr($manifest, $metadatalen); 188 } 189 $ret = array(); 190 $offset = 0; 191 for ($i = 0; $i < $info[1]; $i++) { 192 if (strlen($manifest) < 4) { 193 if (isset($savepath)) { 194 $errors[] = new PHP_Archive_ExceptionExtended( 195 PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array( 196 'archive' => $this->_archiveName, 'last' => $savepath, 197 'current' => '*unknown*', 'size' => $info[1], 'cur' => $i)); 198 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 199 } else { 200 $errors[] = new PHP_Archive_ExceptionExtended( 201 PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array( 202 'archive' => $this->_archiveName, 'last' => '*none*', 203 'current' => '*unknown*', 'size' => $info[1], 'cur' => $i)); 204 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 205 } 206 } 207 // length of the file name 208 $len = unpack('V', substr($manifest, 0, 4)); 209 if (strlen($manifest) < $len[1] + 4) { 210 if (isset($savepath)) { 211 $errors[] = new PHP_Archive_ExceptionExtended( 212 PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array( 213 'archive' => $this->_archiveName, 'last' => $savepath, 214 'current' => '*unknown*', 'size' => $info[1], 'cur' => $i)); 215 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 216 } else { 217 $errors[] = new PHP_Archive_ExceptionExtended( 218 PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array( 219 'archive' => $this->_archiveName, 'last' => '*none*', 220 'current' => '*unknown*', 'size' => $info[1], 'cur' => $i)); 221 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 222 } 223 } 224 // file name 225 if (!isset($savepath)) { 226 $last = '*none*'; 227 } else { 228 $last = $savepath; 229 } 230 $savepath = substr($manifest, 4, $len[1]); 231 $manifest = substr($manifest, $len[1] + 4); 232 if (strlen($manifest) < 24) { 233 if (isset($savepath)) { 234 $errors[] = new PHP_Archive_ExceptionExtended( 235 PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array( 236 'archive' => $this->_archiveName, 'last' => $last, 237 'current' => $savepath, 'size' => $info[1], 'cur' => $i)); 238 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 239 } else { 240 $errors[] = new PHP_Archive_ExceptionExtended( 241 PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array( 242 'archive' => $this->_archiveName, 'last' => $last, 243 'current' => $savepath, 'size' => $info[1], 'cur' => $i)); 244 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 245 } 246 } 247 // retrieve manifest data: 248 // 0 = uncompressed file size 249 // 1 = save timestamp 250 // 2 = compressed file size 251 // 3 = crc32 252 // 4 = flags 253 // 5 = metadata length 254 $ret[$savepath] = array_values(unpack('Va/Vb/Vc/Vd/Ve/Vf', substr($manifest, 0, 24))); 255 $ret[$savepath][3] = sprintf('%u', $ret[$savepath][3] 256 & 0xffffffff); 257 $manifest = substr($manifest, 24); 258 if ($ret[$savepath][5]) { 259 if (strlen($manifest) < $ret[$savepath][5]) { 260 $errors[] = new PHP_Archive_ExceptionExtended( 261 PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, 262 array('archive' => $this->_archiveName, 'last' => $last, 263 'current' => $savepath, 'size' => $info[1], 'cur' => $i)); 264 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . 265 '"', $errors); 266 } 267 $ret[$savepath][6] = unserialize(fread($fp, $ret[$savepath][5])); 268 } 269 $ret[$savepath][7] = $offset; 270 $offset += $ret[$savepath][2]; 271 } 272 $this->_manifest = $ret; 273 $this->_fileStart = ftell($fp); 274 foreach ($this->_manifest as $path => $info) { 275 $currentFilename = $path; 276 $internalFileLength = $info[2]; 277 // seek to offset of file header within the .phar 278 if (fseek($fp, $this->_fileStart + $info[7])) { 279 $errors[] = new PHP_Archive_ExceptionExtended( 280 PHP_Archive_ExceptionExtended::FILELOCATIONINVALID, 281 array('archive' => $this->_archiveName, 'file' => $path, 'loc' => $this->_fileStart + $info[7], 282 'size' => filesize($this->_archiveName))); 283 continue; 284 } 285 $temp = array('crc32' => $info[3], 'isize' => $info[0]); 286 $data = ''; 287 $count = $internalFileLength; 288 while ($count) { 289 if ($count < 8192) { 290 $data .= @fread($fp, $count); 291 $count = 0; 292 } else { 293 $count -= 8192; 294 $data .= @fread($fp, 8192); 295 } 296 } 297 if ($info[4] & self::GZ) { 298 $data = @gzinflate($data); 299 if ($data === false) { 300 $errors[] = new PHP_Archive_ExceptionExtended( 301 PHP_Archive_ExceptionExtended::FILECORRUPTEDGZ, 302 array('archive' => $this->_archiveName, 'file' => $path, 'loc' => $this->_fileStart + $info[2])); 303 } 304 } 305 if ($info[4] & self::BZ2) { 306 $data = @bzdecompress($data, true); 307 if ($data === false) { 308 $errors[] = new PHP_Archive_ExceptionExtended( 309 PHP_Archive_ExceptionExtended::FILECORRUPTEDBZ2, 310 array('archive' => $this->_archiveName, 'file' => $path, 'loc' => $this->_fileStart + $info[2])); 311 } 312 } 313 if ($temp['isize'] != strlen($data)) { 314 $errors[] = new PHP_Archive_ExceptionExtended( 315 PHP_Archive_ExceptionExtended::FILECORRUPTEDSIZE, 316 array('archive' => $this->_archiveName, 'file' => $path, 'expected' => $temp['isize'], 317 'actual' => strlen($data))); 318 } 319 if ($temp['crc32'] != sprintf("%u", crc32($data) & 0xffffffff)) { 320 $errors[] = new PHP_Archive_ExceptionExtended( 321 PHP_Archive_ExceptionExtended::FILECORRUPTEDCRC, 322 array('archive' => $this->_archiveName, 'file' => $path, 'expected' => $temp['crc32'], 323 'actual' => crc32($data))); 324 } 325 } 326 if ($this->_flags & self::SIG) { 327 do { 328 $end = ftell($fp); 329 $data = fread($fp, 28); 330 if (substr($data, strlen($data) - 4) != 'GBMB') { 331 $errors[] = new PHP_Archive_ExceptionExtended( 332 PHP_Archive_ExceptionExtended::NOSIGNATUREMAGIC, 333 array('archive' => $this->_archiveName)); 334 break; 335 } 336 $type = unpack('V', substr($data, strlen($data) - 8, 4)); 337 $all = file_get_contents($this->_archiveName); 338 switch ($type[1]) { 339 case self::MD5 : 340 $hash = substr($all, strlen($all) - 16 - 8, 16); 341 $all = substr($all, 0, strlen($all) - 16 - 8); 342 if (md5(substr($all, 0, $end), true) != $hash) { 343 $errors[] = new PHP_Archive_ExceptionExtended( 344 PHP_Archive_ExceptionExtended::BADSIGNATURE, 345 array('archive' => $this->_archiveName)); 346 } else { 347 $this->_sigtype = 'MD5'; 348 $this->_signature = md5($all); 349 } 350 break; 351 case self::SHA1 : 352 $hash = substr($all, strlen($all) - 20 - 8, 20); 353 if (sha1(substr($all, 0, $end), true) != $hash) { 354 $errors[] = new PHP_Archive_ExceptionExtended( 355 PHP_Archive_ExceptionExtended::BADSIGNATURE, 356 array('archive' => $this->_archiveName)); 357 } else { 358 $this->_sigtype = 'SHA1'; 359 $this->_signature = sha1($all); 360 } 361 break; 362 default : 363 $errors[] = new PHP_Archive_ExceptionExtended( 364 PHP_Archive_ExceptionExtended::UNKNOWNSIGTYPE, 365 array('archive' => $this->_archiveName, 366 'type' => bin2hex(substr($data, strlen($data) - 8, 4)))); 367 break; 368 } 369 } while (false); 370 } 371 @fclose($fp); 372 if (count($errors)) { 373 throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors); 374 } 375 } 376 377 /** 378 * Display information on a phar 379 * 380 * @param bool 381 */ 382 public function dump($return_array = false) 383 { 384 if (!$return_array) { 385 echo $this; 386 return; 387 } 388 $filesize = filesize($this->_archiveName); 389 $ret = array( 390 'Phar name' => $this->_archiveName, 391 'Size' => $filesize, 392 'API version' => $this->_apiVersion, 393 'Manifest size (bytes)' => $this->_manifestSize, 394 'Manifest entries' => count($this->_manifest), 395 'Alias' => $this->_alias, 396 'Phar Metadata' => var_export($this->_metadata, true), 397 'Global compressed flag' => bin2hex($this->_flags), 398 ); 399 if ($this->_signature) { 400 $ret['Signature Type'] = $this->_sigtype; 401 $ret['Signature'] = $this->_signature; 402 } 403 // 0 = uncompressed file size 404 // 1 = save timestamp 405 // 2 = compressed file size 406 // 3 = crc32 407 // 4 = flags 408 // 5 = meta-data length 409 // 6 = meta-data 410 $offset = 0; 411 foreach ($this->_manifest as $file => $info) { 412 $ret['File phar://' . $this->_alias . '/' . $file . ' size'] = $info[0]; 413 $ret['File phar://' . $this->_alias . '/' . $file . ' save date'] = 414 date('Y-m-d H:i', $info[1]); 415 $ret['File phar://' . $this->_alias . '/' . $file . ' crc'] = $info[3]; 416 $ret['File phar://' . $this->_alias . '/' . $file . ' size in archive'] = $info[2]; 417 $ret['File phar://' . $this->_alias . '/' . $file . ' offset in archive'] = $offset; 418 $ret['File phar://' . $this->_alias . '/' . $file . ' meta-data length'] = $info[5]; 419 $ret['File phar://' . $this->_alias . '/' . $file . ' meta-data'] = 420 var_export($info[6], true); 421 $ret['File phar://' . $this->_alias . '/' . $file . ' GZ compressed'] = 422 $info[4] & self::GZ ? 'yes' : 'no'; 423 $ret['File phar://' . $this->_alias . '/' . $file . ' BZ2 compressed'] = 424 $info[4] & self::BZ2 ? 'yes' : 'no'; 425 $offset += $info[2]; 426 } 427 return $ret; 428 } 429 430 public function __toString() 431 { 432 $ret = $this->dump(true); 433 if ($this->_html) { 434 array_walk($ret, function(&$a, $b) { $a = "<strong>$b:</strong> $a"; }); 435 $ret = implode("<br />\n", $ret); 436 } else { 437 array_walk($ret, function(&$a, $b) { $a = "$b: $a"; }); 438 $ret = implode("\n", $ret); 439 } 440 return $ret; 441 } 442 443 /** 444 * Extract the .phar to a particular location 445 * 446 * @param string $toHere 447 */ 448 public function unPhar($toHere) 449 { 450 451 } 452 453 /** 454 * Re-make the phar from a previously after having done work on an unPharred phar 455 * 456 * @param string $fromHere 457 */ 458 public function rePhar($fromHere) 459 { 460 461 } 462 463 /** 464 * For display of data in a browser 465 * 466 * @return PHP_Archive_Manager 467 */ 468 public function inHtml() 469 { 470 $this->_html = true; 471 $a = clone $this; 472 $this->_html = false; 473 return $a; 474 } 475} 476?> 477