1<?php 2 3namespace splitbrain\PHPArchive; 4 5/** 6 * Class Tar 7 * 8 * Creates or extracts Tar archives. Supports gz and bzip compression 9 * 10 * Long pathnames (>100 chars) are supported in POSIX ustar and GNU longlink formats. 11 * 12 * @author Andreas Gohr <andi@splitbrain.org> 13 * @package splitbrain\PHPArchive 14 * @license MIT 15 */ 16class Tar extends Archive 17{ 18 const READ_CHUNK_SIZE = 1048576; // 1MB 19 20 protected $file = ''; 21 protected $comptype = Archive::COMPRESS_AUTO; 22 protected $complevel = 9; 23 protected $fh; 24 protected $memory = ''; 25 protected $closed = true; 26 protected $writeaccess = false; 27 protected $position = 0; 28 protected $contentUntil = 0; 29 protected $skipUntil = 0; 30 31 /** 32 * Sets the compression to use 33 * 34 * @param int $level Compression level (0 to 9) 35 * @param int $type Type of compression to use (use COMPRESS_* constants) 36 * @throws ArchiveIllegalCompressionException 37 */ 38 public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO) 39 { 40 $this->compressioncheck($type); 41 if ($level < -1 || $level > 9) { 42 throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9'); 43 } 44 $this->comptype = $type; 45 $this->complevel = $level; 46 if($level == 0) $this->comptype = Archive::COMPRESS_NONE; 47 if($type == Archive::COMPRESS_NONE) $this->complevel = 0; 48 } 49 50 /** 51 * Open an existing TAR file for reading 52 * 53 * @param string $file 54 * @throws ArchiveIOException 55 * @throws ArchiveIllegalCompressionException 56 */ 57 public function open($file) 58 { 59 $this->file = $file; 60 61 // update compression to mach file 62 if ($this->comptype == Tar::COMPRESS_AUTO) { 63 $this->setCompression($this->complevel, $this->filetype($file)); 64 } 65 66 // open file handles 67 if ($this->comptype === Archive::COMPRESS_GZIP) { 68 $this->fh = @gzopen($this->file, 'rb'); 69 } elseif ($this->comptype === Archive::COMPRESS_BZIP) { 70 $this->fh = @bzopen($this->file, 'r'); 71 } else { 72 $this->fh = @fopen($this->file, 'rb'); 73 } 74 75 if (!$this->fh) { 76 throw new ArchiveIOException('Could not open file for reading: '.$this->file); 77 } 78 $this->closed = false; 79 $this->position = 0; 80 } 81 82 /** 83 * Read the contents of a TAR archive 84 * 85 * This function lists the files stored in the archive 86 * 87 * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams. 88 * Reopen the file with open() again if you want to do additional operations 89 * 90 * @throws ArchiveIOException 91 * @throws ArchiveCorruptedException 92 * @returns FileInfo[] 93 */ 94 public function contents() 95 { 96 $result = array(); 97 98 foreach ($this->yieldContents() as $fileinfo) { 99 $result[] = $fileinfo; 100 } 101 102 return $result; 103 } 104 105 /** 106 * Read the contents of a TAR archive and return each entry using yield 107 * for memory efficiency. 108 * 109 * @see contents() 110 * @throws ArchiveIOException 111 * @throws ArchiveCorruptedException 112 * @returns FileInfo[] 113 */ 114 public function yieldContents() 115 { 116 if ($this->closed || !$this->file) { 117 throw new ArchiveIOException('Can not read from a closed archive'); 118 } 119 120 while ($read = $this->readbytes(512)) { 121 $header = $this->parseHeader($read); 122 if (!is_array($header)) { 123 continue; 124 } 125 126 $this->contentUntil = $this->position + $header['size']; 127 $this->skipUntil = $this->position + ceil($header['size'] / 512) * 512; 128 129 yield $this->header2fileinfo($header); 130 131 $skip = $this->skipUntil - $this->position; 132 if ($skip > 0) { 133 $this->skipbytes($skip); 134 } 135 } 136 137 $this->close(); 138 } 139 140 /** 141 * Reads content of a current archive entry. 142 * 143 * Works only when iterating trough the archive using the generator returned 144 * by the yieldContents(). 145 * 146 * @param int $length maximum number of bytes to read 147 * 148 * @return string 149 */ 150 public function readCurrentEntry($length = PHP_INT_MAX) 151 { 152 $length = (int) min($length, $this->contentUntil - $this->position); 153 if ($length === 0) { 154 return ''; 155 } 156 return $this->readbytes($length); 157 } 158 159 /** 160 * Extract an existing TAR archive 161 * 162 * The $strip parameter allows you to strip a certain number of path components from the filenames 163 * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when 164 * an integer is passed as $strip. 165 * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix, 166 * the prefix will be stripped. It is recommended to give prefixes with a trailing slash. 167 * 168 * By default this will extract all files found in the archive. You can restrict the output using the $include 169 * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If 170 * $include is set only files that match this expression will be extracted. Files that match the $exclude 171 * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against 172 * stripped filenames as described above. 173 * 174 * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams. 175 * Reopen the file with open() again if you want to do additional operations 176 * 177 * @param string $outdir the target directory for extracting 178 * @param int|string $strip either the number of path components or a fixed prefix to strip 179 * @param string $exclude a regular expression of files to exclude 180 * @param string $include a regular expression of files to include 181 * @throws ArchiveIOException 182 * @throws ArchiveCorruptedException 183 * @return FileInfo[] 184 */ 185 public function extract($outdir, $strip = '', $exclude = '', $include = '') 186 { 187 if ($this->closed || !$this->file) { 188 throw new ArchiveIOException('Can not read from a closed archive'); 189 } 190 191 $outdir = rtrim($outdir, '/'); 192 @mkdir($outdir, 0777, true); 193 if (!is_dir($outdir)) { 194 throw new ArchiveIOException("Could not create directory '$outdir'"); 195 } 196 197 $extracted = array(); 198 while ($dat = $this->readbytes(512)) { 199 // read the file header 200 $header = $this->parseHeader($dat); 201 if (!is_array($header)) { 202 continue; 203 } 204 $fileinfo = $this->header2fileinfo($header); 205 206 // apply strip rules 207 $fileinfo->strip($strip); 208 209 // skip unwanted files 210 if (!strlen($fileinfo->getPath()) || !$fileinfo->matchExpression($include, $exclude)) { 211 $this->skipbytes(ceil($header['size'] / 512) * 512); 212 continue; 213 } 214 215 // create output directory 216 $output = $outdir.'/'.$fileinfo->getPath(); 217 $directory = ($fileinfo->getIsdir()) ? $output : dirname($output); 218 if (!file_exists($directory)) { 219 mkdir($directory, 0777, true); 220 } 221 222 // extract data 223 if (!$fileinfo->getIsdir()) { 224 $fp = @fopen($output, "wb"); 225 if (!$fp) { 226 throw new ArchiveIOException('Could not open file for writing: '.$output); 227 } 228 229 $size = floor($header['size'] / 512); 230 for ($i = 0; $i < $size; $i++) { 231 fwrite($fp, $this->readbytes(512), 512); 232 } 233 if (($header['size'] % 512) != 0) { 234 fwrite($fp, $this->readbytes(512), $header['size'] % 512); 235 } 236 237 fclose($fp); 238 @touch($output, $fileinfo->getMtime()); 239 @chmod($output, $fileinfo->getMode()); 240 } else { 241 $this->skipbytes(ceil($header['size'] / 512) * 512); // the size is usually 0 for directories 242 } 243 244 if(is_callable($this->callback)) { 245 call_user_func($this->callback, $fileinfo); 246 } 247 $extracted[] = $fileinfo; 248 } 249 250 $this->close(); 251 return $extracted; 252 } 253 254 /** 255 * Create a new TAR file 256 * 257 * If $file is empty, the tar file will be created in memory 258 * 259 * @param string $file 260 * @throws ArchiveIOException 261 * @throws ArchiveIllegalCompressionException 262 */ 263 public function create($file = '') 264 { 265 $this->file = $file; 266 $this->memory = ''; 267 $this->fh = 0; 268 269 if ($this->file) { 270 // determine compression 271 if ($this->comptype == Archive::COMPRESS_AUTO) { 272 $this->setCompression($this->complevel, $this->filetype($file)); 273 } 274 275 if ($this->comptype === Archive::COMPRESS_GZIP) { 276 $this->fh = @gzopen($this->file, 'wb'.$this->complevel); 277 } elseif ($this->comptype === Archive::COMPRESS_BZIP) { 278 $this->fh = @bzopen($this->file, 'w'); 279 } else { 280 $this->fh = @fopen($this->file, 'wb'); 281 } 282 283 if (!$this->fh) { 284 throw new ArchiveIOException('Could not open file for writing: '.$this->file); 285 } 286 } 287 $this->writeaccess = true; 288 $this->closed = false; 289 } 290 291 /** 292 * Add a file to the current TAR archive using an existing file in the filesystem 293 * 294 * @param string $file path to the original file 295 * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original 296 * @throws ArchiveCorruptedException when the file changes while reading it, the archive will be corrupt and should be deleted 297 * @throws ArchiveIOException there was trouble reading the given file, it was not added 298 * @throws FileInfoException trouble reading file info, it was not added 299 */ 300 public function addFile($file, $fileinfo = '') 301 { 302 if (is_string($fileinfo)) { 303 $fileinfo = FileInfo::fromPath($file, $fileinfo); 304 } 305 306 if ($this->closed) { 307 throw new ArchiveIOException('Archive has been closed, files can no longer be added'); 308 } 309 310 // create file header 311 $this->writeFileHeader($fileinfo); 312 313 // write data, but only if we have data to write. 314 // note: on Windows fopen() on a directory will fail, so we prevent 315 // errors on Windows by testing if we have data to write. 316 if (!$fileinfo->getIsdir() && $fileinfo->getSize() > 0) { 317 $read = 0; 318 $fp = @fopen($file, 'rb'); 319 if (!$fp) { 320 throw new ArchiveIOException('Could not open file for reading: ' . $file); 321 } 322 while (!feof($fp)) { 323 // for performance reasons read bigger chunks at once 324 $data = fread($fp, self::READ_CHUNK_SIZE); 325 if ($data === false) { 326 break; 327 } 328 if ($data === '') { 329 break; 330 } 331 $dataLen = strlen($data); 332 $read += $dataLen; 333 // how much of data read fully fills 512-byte blocks? 334 $passLen = ($dataLen >> 9) << 9; 335 if ($passLen === $dataLen) { 336 // all - just write the data 337 $this->writebytes($data); 338 } else { 339 // directly write what fills 512-byte blocks fully 340 $this->writebytes(substr($data, 0, $passLen)); 341 // pad the reminder to 512 bytes 342 $this->writebytes(pack("a512", substr($data, $passLen))); 343 } 344 } 345 fclose($fp); 346 347 if ($read != $fileinfo->getSize()) { 348 $this->close(); 349 throw new ArchiveCorruptedException("The size of $file changed while reading, archive corrupted. read $read expected ".$fileinfo->getSize()); 350 } 351 } 352 353 if(is_callable($this->callback)) { 354 call_user_func($this->callback, $fileinfo); 355 } 356 } 357 358 /** 359 * Add a file to the current TAR archive using the given $data as content 360 * 361 * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data 362 * @param string $data binary content of the file to add 363 * @throws ArchiveIOException 364 */ 365 public function addData($fileinfo, $data) 366 { 367 if (is_string($fileinfo)) { 368 $fileinfo = new FileInfo($fileinfo); 369 } 370 371 if ($this->closed) { 372 throw new ArchiveIOException('Archive has been closed, files can no longer be added'); 373 } 374 375 $len = strlen($data); 376 $fileinfo->setSize($len); 377 $this->writeFileHeader($fileinfo); 378 379 // write directly everything but the last block which needs padding 380 $passLen = ($len >> 9) << 9; 381 $this->writebytes(substr($data, 0, $passLen)); 382 if ($passLen < $len) { 383 $this->writebytes(pack("a512", substr($data, $passLen, 512))); 384 } 385 386 if (is_callable($this->callback)) { 387 call_user_func($this->callback, $fileinfo); 388 } 389 } 390 391 /** 392 * Add the closing footer to the archive if in write mode, close all file handles 393 * 394 * After a call to this function no more data can be added to the archive, for 395 * read access no reading is allowed anymore 396 * 397 * "Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which 398 * consists of two 512 blocks of zero bytes" 399 * 400 * @link http://www.gnu.org/software/tar/manual/html_chapter/tar_8.html#SEC134 401 * @throws ArchiveIOException 402 */ 403 public function close() 404 { 405 if ($this->closed) { 406 return; 407 } // we did this already 408 409 // write footer 410 if ($this->writeaccess) { 411 $this->writebytes(pack("a512", "")); 412 $this->writebytes(pack("a512", "")); 413 } 414 415 // close file handles 416 if ($this->file) { 417 if ($this->comptype === Archive::COMPRESS_GZIP) { 418 gzclose($this->fh); 419 } elseif ($this->comptype === Archive::COMPRESS_BZIP) { 420 bzclose($this->fh); 421 } else { 422 fclose($this->fh); 423 } 424 425 $this->file = ''; 426 $this->fh = 0; 427 } 428 429 $this->writeaccess = false; 430 $this->closed = true; 431 } 432 433 /** 434 * Returns the created in-memory archive data 435 * 436 * This implicitly calls close() on the Archive 437 * @throws ArchiveIOException 438 */ 439 public function getArchive() 440 { 441 $this->close(); 442 443 if ($this->comptype === Archive::COMPRESS_AUTO) { 444 $this->comptype = Archive::COMPRESS_NONE; 445 } 446 447 if ($this->comptype === Archive::COMPRESS_GZIP) { 448 return gzencode($this->memory, $this->complevel); 449 } 450 if ($this->comptype === Archive::COMPRESS_BZIP) { 451 return bzcompress($this->memory); 452 } 453 return $this->memory; 454 } 455 456 /** 457 * Save the created in-memory archive data 458 * 459 * Note: It more memory effective to specify the filename in the create() function and 460 * let the library work on the new file directly. 461 * 462 * @param string $file 463 * @throws ArchiveIOException 464 * @throws ArchiveIllegalCompressionException 465 */ 466 public function save($file) 467 { 468 if ($this->comptype === Archive::COMPRESS_AUTO) { 469 $this->setCompression($this->complevel, $this->filetype($file)); 470 } 471 472 if (!@file_put_contents($file, $this->getArchive())) { 473 throw new ArchiveIOException('Could not write to file: '.$file); 474 } 475 } 476 477 /** 478 * Read from the open file pointer 479 * 480 * @param int $length bytes to read 481 * @return string 482 */ 483 protected function readbytes($length) 484 { 485 if ($this->comptype === Archive::COMPRESS_GZIP) { 486 $ret = @gzread($this->fh, $length); 487 } elseif ($this->comptype === Archive::COMPRESS_BZIP) { 488 $ret = @bzread($this->fh, $length); 489 } else { 490 $ret = @fread($this->fh, $length); 491 } 492 $this->position += strlen($ret); 493 return $ret; 494 } 495 496 /** 497 * Write to the open filepointer or memory 498 * 499 * @param string $data 500 * @throws ArchiveIOException 501 * @return int number of bytes written 502 */ 503 protected function writebytes($data) 504 { 505 if (!$this->file) { 506 $this->memory .= $data; 507 $written = strlen($data); 508 } elseif ($this->comptype === Archive::COMPRESS_GZIP) { 509 $written = @gzwrite($this->fh, $data); 510 } elseif ($this->comptype === Archive::COMPRESS_BZIP) { 511 $written = @bzwrite($this->fh, $data); 512 } else { 513 $written = @fwrite($this->fh, $data); 514 } 515 if ($written === false) { 516 throw new ArchiveIOException('Failed to write to archive stream'); 517 } 518 return $written; 519 } 520 521 /** 522 * Skip forward in the open file pointer 523 * 524 * This is basically a wrapper around seek() (and a workaround for bzip2) 525 * 526 * @param int $bytes seek to this position 527 */ 528 protected function skipbytes($bytes) 529 { 530 if ($this->comptype === Archive::COMPRESS_GZIP) { 531 @gzseek($this->fh, $bytes, SEEK_CUR); 532 } elseif ($this->comptype === Archive::COMPRESS_BZIP) { 533 // there is no seek in bzip2, we simply read on 534 // bzread allows to read a max of 8kb at once 535 while($bytes) { 536 $toread = min(8192, $bytes); 537 @bzread($this->fh, $toread); 538 $bytes -= $toread; 539 } 540 } else { 541 @fseek($this->fh, $bytes, SEEK_CUR); 542 } 543 $this->position += $bytes; 544 } 545 546 /** 547 * Write the given file meta data as header 548 * 549 * @param FileInfo $fileinfo 550 * @throws ArchiveIOException 551 */ 552 protected function writeFileHeader(FileInfo $fileinfo) 553 { 554 $this->writeRawFileHeader( 555 $fileinfo->getPath(), 556 $fileinfo->getUid(), 557 $fileinfo->getGid(), 558 $fileinfo->getMode(), 559 $fileinfo->getSize(), 560 $fileinfo->getMtime(), 561 $fileinfo->getIsdir() ? '5' : '0' 562 ); 563 } 564 565 /** 566 * Write a file header to the stream 567 * 568 * @param string $name 569 * @param int $uid 570 * @param int $gid 571 * @param int $perm 572 * @param int $size 573 * @param int $mtime 574 * @param string $typeflag Set to '5' for directories 575 * @throws ArchiveIOException 576 */ 577 protected function writeRawFileHeader($name, $uid, $gid, $perm, $size, $mtime, $typeflag = '') 578 { 579 // handle filename length restrictions 580 $prefix = ''; 581 $namelen = strlen($name); 582 if ($namelen > 100) { 583 $file = basename($name); 584 $dir = dirname($name); 585 if (strlen($file) > 100 || strlen($dir) > 155) { 586 // we're still too large, let's use GNU longlink 587 $this->writeRawFileHeader('././@LongLink', 0, 0, 0, $namelen, 0, 'L'); 588 for ($s = 0; $s < $namelen; $s += 512) { 589 $this->writebytes(pack("a512", substr($name, $s, 512))); 590 } 591 $name = substr($name, 0, 100); // cut off name 592 } else { 593 // we're fine when splitting, use POSIX ustar 594 $prefix = $dir; 595 $name = $file; 596 } 597 } 598 599 // values are needed in octal 600 $uid = sprintf("%6s ", decoct($uid)); 601 $gid = sprintf("%6s ", decoct($gid)); 602 $perm = sprintf("%6s ", decoct($perm)); 603 $size = self::numberEncode($size, 12); 604 $mtime = self::numberEncode($size, 12); 605 606 $data_first = pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime); 607 $data_last = pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, ""); 608 609 for ($i = 0, $chks = 0; $i < 148; $i++) { 610 $chks += ord($data_first[$i]); 611 } 612 613 for ($i = 156, $chks += 256, $j = 0; $i < 512; $i++, $j++) { 614 $chks += ord($data_last[$j]); 615 } 616 617 $this->writebytes($data_first); 618 619 $chks = pack("a8", sprintf("%6s ", decoct($chks))); 620 $this->writebytes($chks.$data_last); 621 } 622 623 /** 624 * Decode the given tar file header 625 * 626 * @param string $block a 512 byte block containing the header data 627 * @return array|false returns false when this was a null block 628 * @throws ArchiveCorruptedException 629 */ 630 protected function parseHeader($block) 631 { 632 if (!$block || strlen($block) != 512) { 633 throw new ArchiveCorruptedException('Unexpected length of header'); 634 } 635 636 // null byte blocks are ignored 637 if(trim($block) === '') return false; 638 639 for ($i = 0, $chks = 0; $i < 148; $i++) { 640 $chks += ord($block[$i]); 641 } 642 643 for ($i = 156, $chks += 256; $i < 512; $i++) { 644 $chks += ord($block[$i]); 645 } 646 647 $header = @unpack( 648 "a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix", 649 $block 650 ); 651 if (!$header) { 652 throw new ArchiveCorruptedException('Failed to parse header'); 653 } 654 655 $return['checksum'] = OctDec(trim($header['checksum'])); 656 if ($return['checksum'] != $chks) { 657 throw new ArchiveCorruptedException('Header does not match its checksum'); 658 } 659 660 $return['filename'] = trim($header['filename']); 661 $return['perm'] = OctDec(trim($header['perm'])); 662 $return['uid'] = OctDec(trim($header['uid'])); 663 $return['gid'] = OctDec(trim($header['gid'])); 664 $return['size'] = self::numberDecode($header['size']); 665 $return['mtime'] = self::numberDecode($header['mtime']); 666 $return['typeflag'] = $header['typeflag']; 667 $return['link'] = trim($header['link']); 668 $return['uname'] = trim($header['uname']); 669 $return['gname'] = trim($header['gname']); 670 671 // Handle ustar Posix compliant path prefixes 672 if (trim($header['prefix'])) { 673 $return['filename'] = trim($header['prefix']).'/'.$return['filename']; 674 } 675 676 // Handle Long-Link entries from GNU Tar 677 if ($return['typeflag'] == 'L') { 678 // following data block(s) is the filename 679 $filename = trim($this->readbytes(ceil($return['size'] / 512) * 512)); 680 // next block is the real header 681 $block = $this->readbytes(512); 682 $return = $this->parseHeader($block); 683 // overwrite the filename 684 $return['filename'] = $filename; 685 } 686 687 return $return; 688 } 689 690 /** 691 * Creates a FileInfo object from the given parsed header 692 * 693 * @param $header 694 * @return FileInfo 695 */ 696 protected function header2fileinfo($header) 697 { 698 $fileinfo = new FileInfo(); 699 $fileinfo->setPath($header['filename']); 700 $fileinfo->setMode($header['perm']); 701 $fileinfo->setUid($header['uid']); 702 $fileinfo->setGid($header['gid']); 703 $fileinfo->setSize($header['size']); 704 $fileinfo->setMtime($header['mtime']); 705 $fileinfo->setOwner($header['uname']); 706 $fileinfo->setGroup($header['gname']); 707 $fileinfo->setIsdir((bool) $header['typeflag']); 708 709 return $fileinfo; 710 } 711 712 /** 713 * Checks if the given compression type is available and throws an exception if not 714 * 715 * @param $comptype 716 * @throws ArchiveIllegalCompressionException 717 */ 718 protected function compressioncheck($comptype) 719 { 720 if ($comptype === Archive::COMPRESS_GZIP && !function_exists('gzopen')) { 721 throw new ArchiveIllegalCompressionException('No gzip support available'); 722 } 723 724 if ($comptype === Archive::COMPRESS_BZIP && !function_exists('bzopen')) { 725 throw new ArchiveIllegalCompressionException('No bzip2 support available'); 726 } 727 } 728 729 /** 730 * Guesses the wanted compression from the given file 731 * 732 * Uses magic bytes for existing files, the file extension otherwise 733 * 734 * You don't need to call this yourself. It's used when you pass Archive::COMPRESS_AUTO somewhere 735 * 736 * @param string $file 737 * @return int 738 */ 739 public function filetype($file) 740 { 741 // for existing files, try to read the magic bytes 742 if(file_exists($file) && is_readable($file) && filesize($file) > 5) { 743 $fh = @fopen($file, 'rb'); 744 if(!$fh) return false; 745 $magic = fread($fh, 5); 746 fclose($fh); 747 748 if(strpos($magic, "\x42\x5a") === 0) return Archive::COMPRESS_BZIP; 749 if(strpos($magic, "\x1f\x8b") === 0) return Archive::COMPRESS_GZIP; 750 } 751 752 // otherwise rely on file name 753 $file = strtolower($file); 754 if (substr($file, -3) == '.gz' || substr($file, -4) == '.tgz') { 755 return Archive::COMPRESS_GZIP; 756 } elseif (substr($file, -4) == '.bz2' || substr($file, -4) == '.tbz') { 757 return Archive::COMPRESS_BZIP; 758 } 759 760 return Archive::COMPRESS_NONE; 761 } 762 763 /** 764 * Decodes numeric values according to the 765 * https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions 766 * (basically with support for big numbers) 767 * 768 * @param string $field 769 * $return int 770 */ 771 static public function numberDecode($field) 772 { 773 $firstByte = ord(substr($field, 0, 1)); 774 if ($firstByte === 255) { 775 $value = -1 << (8 * strlen($field)); 776 $shift = 0; 777 for ($i = strlen($field) - 1; $i >= 0; $i--) { 778 $value += ord(substr($field, $i, 1)) << $shift; 779 $shift += 8; 780 } 781 } elseif ($firstByte === 128) { 782 $value = 0; 783 $shift = 0; 784 for ($i = strlen($field) - 1; $i > 0; $i--) { 785 $value += ord(substr($field, $i, 1)) << $shift; 786 $shift += 8; 787 } 788 } else { 789 $value = octdec(trim($field)); 790 } 791 return $value; 792 } 793 794 /** 795 * Encodes numeric values according to the 796 * https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions 797 * (basically with support for big numbers) 798 * 799 * @param int $value 800 * @param int $length field length 801 * @return string 802 */ 803 static public function numberEncode($value, $length) 804 { 805 // old implementations leave last byte empty 806 // octal encoding encodes three bits per byte 807 $maxValue = 1 << (($length - 1) * 3); 808 if ($value < 0) { 809 // PHP already stores integers as 2's complement 810 $value = pack(PHP_INT_SIZE === 8 ? 'J' : 'N', (int) $value); 811 $encoded = str_repeat(chr(255), max(1, $length - PHP_INT_SIZE)); 812 $encoded .= substr($value, max(0, PHP_INT_SIZE - $length + 1)); 813 } elseif ($value >= $maxValue) { 814 $value = pack(PHP_INT_SIZE === 8 ? 'J' : 'N', (int) $value); 815 $encoded = chr(128) . str_repeat(chr(0), max(0, $length - PHP_INT_SIZE - 1)); 816 $encoded .= substr($value, max(0, PHP_INT_SIZE - $length + 1)); 817 } else { 818 $encoded = sprintf("%" . ($length - 1) . "s ", decoct($value)); 819 } 820 return $encoded; 821 } 822} 823 824