[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/vendor/splitbrain/php-archive/src/ -> Zip.php (source)

   1  <?php
   2  
   3  namespace splitbrain\PHPArchive;
   4  
   5  /**
   6   * Class Zip
   7   *
   8   * Creates or extracts Zip archives
   9   *
  10   * for specs see http://www.pkware.com/appnote
  11   *
  12   * @author  Andreas Gohr <andi@splitbrain.org>
  13   * @package splitbrain\PHPArchive
  14   * @license MIT
  15   */
  16  class Zip extends Archive
  17  {
  18      const LOCAL_FILE_HEADER_CRC_OFFSET = 14;
  19  
  20      protected $file = '';
  21      protected $fh;
  22      protected $memory = '';
  23      protected $closed = true;
  24      protected $writeaccess = false;
  25      protected $ctrl_dir;
  26      protected $complevel = 9;
  27  
  28      /**
  29       * Set the compression level.
  30       *
  31       * Compression Type is ignored for ZIP
  32       *
  33       * You can call this function before adding each file to set differen compression levels
  34       * for each file.
  35       *
  36       * @param int $level Compression level (0 to 9)
  37       * @param int $type  Type of compression to use ignored for ZIP
  38       * @throws ArchiveIllegalCompressionException
  39       */
  40      public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
  41      {
  42          if ($level < -1 || $level > 9) {
  43              throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9');
  44          }
  45          $this->complevel = $level;
  46      }
  47  
  48      /**
  49       * Open an existing ZIP file for reading
  50       *
  51       * @param string $file
  52       * @throws ArchiveIOException
  53       */
  54      public function open($file)
  55      {
  56          $this->file = $file;
  57          $this->fh   = @fopen($this->file, 'rb');
  58          if (!$this->fh) {
  59              throw new ArchiveIOException('Could not open file for reading: '.$this->file);
  60          }
  61          $this->closed = false;
  62      }
  63  
  64      /**
  65       * Read the contents of a ZIP archive
  66       *
  67       * This function lists the files stored in the archive, and returns an indexed array of FileInfo objects
  68       *
  69       * The archive is closed afer reading the contents, for API compatibility with TAR files
  70       * Reopen the file with open() again if you want to do additional operations
  71       *
  72       * @throws ArchiveIOException
  73       * @return FileInfo[]
  74       */
  75      public function contents()
  76      {
  77          $result = array();
  78  
  79          foreach ($this->yieldContents() as $fileinfo) {
  80              $result[] = $fileinfo;
  81          }
  82  
  83          return $result;
  84      }
  85  
  86      /**
  87       * Read the contents of a ZIP archive and return each entry using yield
  88       * for memory efficiency.
  89       *
  90       * @see contents()
  91       * @throws ArchiveIOException
  92       * @return FileInfo[]
  93       */
  94      public function yieldContents()
  95      {
  96          if ($this->closed || !$this->file) {
  97              throw new ArchiveIOException('Can not read from a closed archive');
  98          }
  99  
 100          $centd = $this->readCentralDir();
 101  
 102          @rewind($this->fh);
 103          @fseek($this->fh, $centd['offset']);
 104  
 105          for ($i = 0; $i < $centd['entries']; $i++) {
 106              yield $this->header2fileinfo($this->readCentralFileHeader());
 107          }
 108  
 109          $this->close();
 110      }
 111  
 112      /**
 113       * Extract an existing ZIP archive
 114       *
 115       * The $strip parameter allows you to strip a certain number of path components from the filenames
 116       * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
 117       * an integer is passed as $strip.
 118       * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
 119       * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
 120       *
 121       * By default this will extract all files found in the archive. You can restrict the output using the $include
 122       * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
 123       * $include is set only files that match this expression will be extracted. Files that match the $exclude
 124       * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
 125       * stripped filenames as described above.
 126       *
 127       * @param string     $outdir  the target directory for extracting
 128       * @param int|string $strip   either the number of path components or a fixed prefix to strip
 129       * @param string     $exclude a regular expression of files to exclude
 130       * @param string     $include a regular expression of files to include
 131       * @throws ArchiveIOException
 132       * @return FileInfo[]
 133       */
 134      public function extract($outdir, $strip = '', $exclude = '', $include = '')
 135      {
 136          if ($this->closed || !$this->file) {
 137              throw new ArchiveIOException('Can not read from a closed archive');
 138          }
 139  
 140          $outdir = rtrim($outdir, '/');
 141          @mkdir($outdir, 0777, true);
 142  
 143          $extracted = array();
 144  
 145          $cdir      = $this->readCentralDir();
 146          $pos_entry = $cdir['offset']; // begin of the central file directory
 147  
 148          for ($i = 0; $i < $cdir['entries']; $i++) {
 149              // read file header
 150              @fseek($this->fh, $pos_entry);
 151              $header          = $this->readCentralFileHeader();
 152              $header['index'] = $i;
 153              $pos_entry       = ftell($this->fh); // position of the next file in central file directory
 154              fseek($this->fh, $header['offset']); // seek to beginning of file header
 155              $header   = $this->readFileHeader($header);
 156              $fileinfo = $this->header2fileinfo($header);
 157  
 158              // apply strip rules
 159              $fileinfo->strip($strip);
 160  
 161              // skip unwanted files
 162              if (!strlen($fileinfo->getPath()) || !$fileinfo->matchExpression($include, $exclude)) {
 163                  continue;
 164              }
 165  
 166              $extracted[] = $fileinfo;
 167  
 168              // create output directory
 169              $output    = $outdir.'/'.$fileinfo->getPath();
 170              $directory = ($header['folder']) ? $output : dirname($output);
 171              @mkdir($directory, 0777, true);
 172  
 173              // nothing more to do for directories
 174              if ($fileinfo->getIsdir()) {
 175                  if(is_callable($this->callback)) {
 176                      call_user_func($this->callback, $fileinfo);
 177                  }
 178                  continue;
 179              }
 180  
 181              // compressed files are written to temporary .gz file first
 182              if ($header['compression'] == 0) {
 183                  $extractto = $output;
 184              } else {
 185                  $extractto = $output.'.gz';
 186              }
 187  
 188              // open file for writing
 189              $fp = @fopen($extractto, "wb");
 190              if (!$fp) {
 191                  throw new ArchiveIOException('Could not open file for writing: '.$extractto);
 192              }
 193  
 194              // prepend compression header
 195              if ($header['compression'] != 0) {
 196                  $binary_data = pack(
 197                      'va1a1Va1a1',
 198                      0x8b1f,
 199                      chr($header['compression']),
 200                      chr(0x00),
 201                      time(),
 202                      chr(0x00),
 203                      chr(3)
 204                  );
 205                  fwrite($fp, $binary_data, 10);
 206              }
 207  
 208              // read the file and store it on disk
 209              $size = $header['compressed_size'];
 210              while ($size != 0) {
 211                  $read_size   = ($size < 2048 ? $size : 2048);
 212                  $buffer      = fread($this->fh, $read_size);
 213                  $binary_data = pack('a'.$read_size, $buffer);
 214                  fwrite($fp, $binary_data, $read_size);
 215                  $size -= $read_size;
 216              }
 217  
 218              // finalize compressed file
 219              if ($header['compression'] != 0) {
 220                  $binary_data = pack('VV', $header['crc'], $header['size']);
 221                  fwrite($fp, $binary_data, 8);
 222              }
 223  
 224              // close file
 225              fclose($fp);
 226  
 227              // unpack compressed file
 228              if ($header['compression'] != 0) {
 229                  $gzp = @gzopen($extractto, 'rb');
 230                  if (!$gzp) {
 231                      @unlink($extractto);
 232                      throw new ArchiveIOException('Failed file extracting. gzip support missing?');
 233                  }
 234                  $fp = @fopen($output, 'wb');
 235                  if (!$fp) {
 236                      throw new ArchiveIOException('Could not open file for writing: '.$extractto);
 237                  }
 238  
 239                  $size = $header['size'];
 240                  while ($size != 0) {
 241                      $read_size   = ($size < 2048 ? $size : 2048);
 242                      $buffer      = gzread($gzp, $read_size);
 243                      $binary_data = pack('a'.$read_size, $buffer);
 244                      @fwrite($fp, $binary_data, $read_size);
 245                      $size -= $read_size;
 246                  }
 247                  fclose($fp);
 248                  gzclose($gzp);
 249                  unlink($extractto); // remove temporary gz file
 250              }
 251  
 252              @touch($output, $fileinfo->getMtime());
 253              //FIXME what about permissions?
 254              if(is_callable($this->callback)) {
 255                  call_user_func($this->callback, $fileinfo);
 256              }
 257          }
 258  
 259          $this->close();
 260          return $extracted;
 261      }
 262  
 263      /**
 264       * Create a new ZIP file
 265       *
 266       * If $file is empty, the zip file will be created in memory
 267       *
 268       * @param string $file
 269       * @throws ArchiveIOException
 270       */
 271      public function create($file = '')
 272      {
 273          $this->file   = $file;
 274          $this->memory = '';
 275          $this->fh     = 0;
 276  
 277          if ($this->file) {
 278              $this->fh = @fopen($this->file, 'wb');
 279  
 280              if (!$this->fh) {
 281                  throw new ArchiveIOException('Could not open file for writing: '.$this->file);
 282              }
 283          }
 284          $this->writeaccess = true;
 285          $this->closed      = false;
 286          $this->ctrl_dir    = array();
 287      }
 288  
 289      /**
 290       * Add a file to the current ZIP archive using an existing file in the filesystem
 291       *
 292       * @param string          $file     path to the original file
 293       * @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
 294       * @throws ArchiveIOException
 295       */
 296  
 297      /**
 298       * Add a file to the current archive using an existing file in the filesystem
 299       *
 300       * @param string $file path to the original file
 301       * @param string|FileInfo $fileinfo either the name to use in archive (string) or a FileInfo oject with all meta data, empty to take from original
 302       * @throws ArchiveIOException
 303       * @throws FileInfoException
 304       */
 305      public function addFile($file, $fileinfo = '')
 306      {
 307          if (is_string($fileinfo)) {
 308              $fileinfo = FileInfo::fromPath($file, $fileinfo);
 309          }
 310  
 311          if ($this->closed) {
 312              throw new ArchiveIOException('Archive has been closed, files can no longer be added');
 313          }
 314  
 315          $fp = @fopen($file, 'rb');
 316          if ($fp === false) {
 317              throw new ArchiveIOException('Could not open file for reading: '.$file);
 318          }
 319  
 320          $offset = $this->dataOffset();
 321          $name   = $fileinfo->getPath();
 322          $time   = $fileinfo->getMtime();
 323  
 324          // write local file header (temporary CRC and size)
 325          $this->writebytes($this->makeLocalFileHeader(
 326              $time,
 327              0,
 328              0,
 329              0,
 330              $name,
 331              (bool) $this->complevel
 332          ));
 333  
 334          // we store no encryption header
 335  
 336          // prepare info, compress and write data to archive
 337          $deflate_context = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => $this->complevel]);
 338          $crc_context = hash_init('crc32b');
 339          $size = $csize = 0;
 340  
 341          while (!feof($fp)) {
 342              $block = fread($fp, 512);
 343  
 344              if ($this->complevel) {
 345                  $is_first_block = $size === 0;
 346                  $is_last_block = feof($fp);
 347  
 348                  if ($is_last_block) {
 349                      $c_block = deflate_add($deflate_context, $block, ZLIB_FINISH);
 350                      // get rid of the compression footer
 351                      $c_block = substr($c_block, 0, -4);
 352                  } else {
 353                      $c_block = deflate_add($deflate_context, $block, ZLIB_NO_FLUSH);
 354                  }
 355  
 356                  // get rid of the compression header
 357                  if ($is_first_block) {
 358                      $c_block = substr($c_block, 2);
 359                  }
 360  
 361                  $csize += strlen($c_block);
 362                  $this->writebytes($c_block);
 363              } else {
 364                  $this->writebytes($block);
 365              }
 366  
 367              $size += strlen($block);
 368              hash_update($crc_context, $block);
 369          }
 370          fclose($fp);
 371  
 372          // update the local file header with the computed CRC and size
 373          $crc = hexdec(hash_final($crc_context));
 374          $csize = $this->complevel ? $csize : $size;
 375          $this->writebytesAt($this->makeCrcAndSize(
 376              $crc,
 377              $size,
 378              $csize
 379          ), $offset + self::LOCAL_FILE_HEADER_CRC_OFFSET);
 380  
 381          // we store no data descriptor
 382  
 383          // add info to central file directory
 384          $this->ctrl_dir[] = $this->makeCentralFileRecord(
 385              $offset,
 386              $time,
 387              $crc,
 388              $size,
 389              $csize,
 390              $name,
 391              (bool) $this->complevel
 392          );
 393  
 394          if(is_callable($this->callback)) {
 395              call_user_func($this->callback, $fileinfo);
 396          }
 397      }
 398  
 399      /**
 400       * Add a file to the current Zip archive using the given $data as content
 401       *
 402       * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
 403       * @param string          $data     binary content of the file to add
 404       * @throws ArchiveIOException
 405       */
 406      public function addData($fileinfo, $data)
 407      {
 408          if (is_string($fileinfo)) {
 409              $fileinfo = new FileInfo($fileinfo);
 410          }
 411  
 412          if ($this->closed) {
 413              throw new ArchiveIOException('Archive has been closed, files can no longer be added');
 414          }
 415  
 416          // prepare info and compress data
 417          $size     = strlen($data);
 418          $crc      = crc32($data);
 419          if ($this->complevel) {
 420              $data = gzcompress($data, $this->complevel);
 421              $data = substr($data, 2, -4); // strip compression headers
 422          }
 423          $csize  = strlen($data);
 424          $offset = $this->dataOffset();
 425          $name   = $fileinfo->getPath();
 426          $time   = $fileinfo->getMtime();
 427  
 428          // write local file header
 429          $this->writebytes($this->makeLocalFileHeader(
 430              $time,
 431              $crc,
 432              $size,
 433              $csize,
 434              $name,
 435              (bool) $this->complevel
 436          ));
 437  
 438          // we store no encryption header
 439  
 440          // write data
 441          $this->writebytes($data);
 442  
 443          // we store no data descriptor
 444  
 445          // add info to central file directory
 446          $this->ctrl_dir[] = $this->makeCentralFileRecord(
 447              $offset,
 448              $time,
 449              $crc,
 450              $size,
 451              $csize,
 452              $name,
 453              (bool) $this->complevel
 454          );
 455  
 456          if(is_callable($this->callback)) {
 457              call_user_func($this->callback, $fileinfo);
 458          }
 459      }
 460  
 461      /**
 462       * Add the closing footer to the archive if in write mode, close all file handles
 463       *
 464       * After a call to this function no more data can be added to the archive, for
 465       * read access no reading is allowed anymore
 466       * @throws ArchiveIOException
 467       */
 468      public function close()
 469      {
 470          if ($this->closed) {
 471              return;
 472          } // we did this already
 473  
 474          if ($this->writeaccess) {
 475              // write central directory
 476              $offset = $this->dataOffset();
 477              $ctrldir = join('', $this->ctrl_dir);
 478              $this->writebytes($ctrldir);
 479  
 480              // write end of central directory record
 481              $this->writebytes("\x50\x4b\x05\x06"); // end of central dir signature
 482              $this->writebytes(pack('v', 0)); // number of this disk
 483              $this->writebytes(pack('v', 0)); // number of the disk with the start of the central directory
 484              $this->writebytes(pack('v',
 485                  count($this->ctrl_dir))); // total number of entries in the central directory on this disk
 486              $this->writebytes(pack('v', count($this->ctrl_dir))); // total number of entries in the central directory
 487              $this->writebytes(pack('V', strlen($ctrldir))); // size of the central directory
 488              $this->writebytes(pack('V',
 489                  $offset)); // offset of start of central directory with respect to the starting disk number
 490              $this->writebytes(pack('v', 0)); // .ZIP file comment length
 491  
 492              $this->ctrl_dir = array();
 493          }
 494  
 495          // close file handles
 496          if ($this->file) {
 497              fclose($this->fh);
 498              $this->file = '';
 499              $this->fh   = 0;
 500          }
 501  
 502          $this->writeaccess = false;
 503          $this->closed      = true;
 504      }
 505  
 506      /**
 507       * Returns the created in-memory archive data
 508       *
 509       * This implicitly calls close() on the Archive
 510       * @throws ArchiveIOException
 511       */
 512      public function getArchive()
 513      {
 514          $this->close();
 515  
 516          return $this->memory;
 517      }
 518  
 519      /**
 520       * Save the created in-memory archive data
 521       *
 522       * Note: It's more memory effective to specify the filename in the create() function and
 523       * let the library work on the new file directly.
 524       *
 525       * @param     $file
 526       * @throws ArchiveIOException
 527       */
 528      public function save($file)
 529      {
 530          if (!@file_put_contents($file, $this->getArchive())) {
 531              throw new ArchiveIOException('Could not write to file: '.$file);
 532          }
 533      }
 534  
 535      /**
 536       * Read the central directory
 537       *
 538       * This key-value list contains general information about the ZIP file
 539       *
 540       * @return array
 541       */
 542      protected function readCentralDir()
 543      {
 544          $size = filesize($this->file);
 545          if ($size < 277) {
 546              $maximum_size = $size;
 547          } else {
 548              $maximum_size = 277;
 549          }
 550  
 551          @fseek($this->fh, $size - $maximum_size);
 552          $pos   = ftell($this->fh);
 553          $bytes = 0x00000000;
 554  
 555          while ($pos < $size) {
 556              $byte  = @fread($this->fh, 1);
 557              $bytes = (($bytes << 8) & 0xFFFFFFFF) | ord($byte);
 558              if ($bytes == 0x504b0506) {
 559                  break;
 560              }
 561              $pos++;
 562          }
 563  
 564          $data = unpack(
 565              'vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size',
 566              fread($this->fh, 18)
 567          );
 568  
 569          if ($data['comment_size'] != 0) {
 570              $centd['comment'] = fread($this->fh, $data['comment_size']);
 571          } else {
 572              $centd['comment'] = '';
 573          }
 574          $centd['entries']      = $data['entries'];
 575          $centd['disk_entries'] = $data['disk_entries'];
 576          $centd['offset']       = $data['offset'];
 577          $centd['disk_start']   = $data['disk_start'];
 578          $centd['size']         = $data['size'];
 579          $centd['disk']         = $data['disk'];
 580          return $centd;
 581      }
 582  
 583      /**
 584       * Read the next central file header
 585       *
 586       * Assumes the current file pointer is pointing at the right position
 587       *
 588       * @return array
 589       */
 590      protected function readCentralFileHeader()
 591      {
 592          $binary_data = fread($this->fh, 46);
 593          $header      = unpack(
 594              'vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset',
 595              $binary_data
 596          );
 597  
 598          if ($header['filename_len'] != 0) {
 599              $header['filename'] = fread($this->fh, $header['filename_len']);
 600          } else {
 601              $header['filename'] = '';
 602          }
 603  
 604          if ($header['extra_len'] != 0) {
 605              $header['extra'] = fread($this->fh, $header['extra_len']);
 606              $header['extradata'] = $this->parseExtra($header['extra']);
 607          } else {
 608              $header['extra'] = '';
 609              $header['extradata'] = array();
 610          }
 611  
 612          if ($header['comment_len'] != 0) {
 613              $header['comment'] = fread($this->fh, $header['comment_len']);
 614          } else {
 615              $header['comment'] = '';
 616          }
 617  
 618          $header['mtime']           = $this->makeUnixTime($header['mdate'], $header['mtime']);
 619          $header['stored_filename'] = $header['filename'];
 620          $header['status']          = 'ok';
 621          if (substr($header['filename'], -1) == '/') {
 622              $header['external'] = 0x41FF0010;
 623          }
 624          $header['folder'] = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0;
 625  
 626          return $header;
 627      }
 628  
 629      /**
 630       * Reads the local file header
 631       *
 632       * This header precedes each individual file inside the zip file. Assumes the current file pointer is pointing at
 633       * the right position already. Enhances the given central header with the data found at the local header.
 634       *
 635       * @param array $header the central file header read previously (see above)
 636       * @return array
 637       */
 638      protected function readFileHeader($header)
 639      {
 640          $binary_data = fread($this->fh, 30);
 641          $data        = unpack(
 642              'vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len',
 643              $binary_data
 644          );
 645  
 646          $header['filename'] = fread($this->fh, $data['filename_len']);
 647          if ($data['extra_len'] != 0) {
 648              $header['extra'] = fread($this->fh, $data['extra_len']);
 649              $header['extradata'] = array_merge($header['extradata'],  $this->parseExtra($header['extra']));
 650          } else {
 651              $header['extra'] = '';
 652              $header['extradata'] = array();
 653          }
 654  
 655          $header['compression'] = $data['compression'];
 656          foreach (array(
 657                       'size',
 658                       'compressed_size',
 659                       'crc'
 660                   ) as $hd) { // On ODT files, these headers are 0. Keep the previous value.
 661              if ($data[$hd] != 0) {
 662                  $header[$hd] = $data[$hd];
 663              }
 664          }
 665          $header['flag']  = $data['flag'];
 666          $header['mtime'] = $this->makeUnixTime($data['mdate'], $data['mtime']);
 667  
 668          $header['stored_filename'] = $header['filename'];
 669          $header['status']          = "ok";
 670          $header['folder']          = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0;
 671          return $header;
 672      }
 673  
 674      /**
 675       * Parse the extra headers into fields
 676       *
 677       * @param string $header
 678       * @return array
 679       */
 680      protected function parseExtra($header)
 681      {
 682          $extra = array();
 683          // parse all extra fields as raw values
 684          while (strlen($header) !== 0) {
 685              $set = unpack('vid/vlen', $header);
 686              $header = substr($header, 4);
 687              $value = substr($header, 0, $set['len']);
 688              $header = substr($header, $set['len']);
 689              $extra[$set['id']] = $value;
 690          }
 691  
 692          // handle known ones
 693          if(isset($extra[0x6375])) {
 694              $extra['utf8comment'] = substr($extra[0x7075], 5); // strip version and crc
 695          }
 696          if(isset($extra[0x7075])) {
 697              $extra['utf8path'] = substr($extra[0x7075], 5); // strip version and crc
 698          }
 699  
 700          return $extra;
 701      }
 702  
 703      /**
 704       * Create fileinfo object from header data
 705       *
 706       * @param $header
 707       * @return FileInfo
 708       */
 709      protected function header2fileinfo($header)
 710      {
 711          $fileinfo = new FileInfo();
 712          $fileinfo->setSize($header['size']);
 713          $fileinfo->setCompressedSize($header['compressed_size']);
 714          $fileinfo->setMtime($header['mtime']);
 715          $fileinfo->setComment($header['comment']);
 716          $fileinfo->setIsdir($header['external'] == 0x41FF0010 || $header['external'] == 16);
 717  
 718          if(isset($header['extradata']['utf8path'])) {
 719              $fileinfo->setPath($header['extradata']['utf8path']);
 720          } else {
 721              $fileinfo->setPath($this->cpToUtf8($header['filename']));
 722          }
 723  
 724          if(isset($header['extradata']['utf8comment'])) {
 725              $fileinfo->setComment($header['extradata']['utf8comment']);
 726          } else {
 727              $fileinfo->setComment($this->cpToUtf8($header['comment']));
 728          }
 729  
 730          return $fileinfo;
 731      }
 732  
 733      /**
 734       * Convert the given CP437 encoded string to UTF-8
 735       *
 736       * Tries iconv with the correct encoding first, falls back to mbstring with CP850 which is
 737       * similar enough. CP437 seems not to be available in mbstring. Lastly falls back to keeping the
 738       * string as is, which is still better than nothing.
 739       *
 740       * On some systems iconv is available, but the codepage is not. We also check for that.
 741       *
 742       * @param $string
 743       * @return string
 744       */
 745      protected function cpToUtf8($string)
 746      {
 747          if (function_exists('iconv') && @iconv_strlen('', 'CP437') !== false) {
 748              return iconv('CP437', 'UTF-8', $string);
 749          } elseif (function_exists('mb_convert_encoding')) {
 750              return mb_convert_encoding($string, 'UTF-8', 'CP850');
 751          } else {
 752              return $string;
 753          }
 754      }
 755  
 756      /**
 757       * Convert the given UTF-8 encoded string to CP437
 758       *
 759       * Same caveats as for cpToUtf8() apply
 760       *
 761       * @param $string
 762       * @return string
 763       */
 764      protected function utf8ToCp($string)
 765      {
 766          // try iconv first
 767          if (function_exists('iconv')) {
 768              $conv = @iconv('UTF-8', 'CP437//IGNORE', $string);
 769              if($conv) return $conv; // it worked
 770          }
 771  
 772          // still here? iconv failed to convert the string. Try another method
 773          // see http://php.net/manual/en/function.iconv.php#108643
 774  
 775          if (function_exists('mb_convert_encoding')) {
 776              return mb_convert_encoding($string, 'CP850', 'UTF-8');
 777          } else {
 778              return $string;
 779          }
 780      }
 781  
 782  
 783      /**
 784       * Write to the open filepointer or memory
 785       *
 786       * @param string $data
 787       * @throws ArchiveIOException
 788       * @return int number of bytes written
 789       */
 790      protected function writebytes($data)
 791      {
 792          if (!$this->file) {
 793              $this->memory .= $data;
 794              $written = strlen($data);
 795          } else {
 796              $written = @fwrite($this->fh, $data);
 797          }
 798          if ($written === false) {
 799              throw new ArchiveIOException('Failed to write to archive stream');
 800          }
 801          return $written;
 802      }
 803  
 804      /**
 805       * Write to the open filepointer or memory at the specified offset
 806       *
 807       * @param string $data
 808       * @param int $offset
 809       * @throws ArchiveIOException
 810       * @return int number of bytes written
 811       */
 812      protected function writebytesAt($data, $offset) {
 813          if (!$this->file) {
 814              $this->memory .= substr_replace($this->memory, $data, $offset);
 815              $written = strlen($data);
 816          } else {
 817              @fseek($this->fh, $offset);
 818              $written = @fwrite($this->fh, $data);
 819              @fseek($this->fh, 0, SEEK_END);
 820          }
 821          if ($written === false) {
 822              throw new ArchiveIOException('Failed to write to archive stream');
 823          }
 824          return $written;
 825      }
 826  
 827      /**
 828       * Current data pointer position
 829       *
 830       * @fixme might need a -1
 831       * @return int
 832       */
 833      protected function dataOffset()
 834      {
 835          if ($this->file) {
 836              return ftell($this->fh);
 837          } else {
 838              return strlen($this->memory);
 839          }
 840      }
 841  
 842      /**
 843       * Create a DOS timestamp from a UNIX timestamp
 844       *
 845       * DOS timestamps start at 1980-01-01, earlier UNIX stamps will be set to this date
 846       *
 847       * @param $time
 848       * @return int
 849       */
 850      protected function makeDosTime($time)
 851      {
 852          $timearray = getdate($time);
 853          if ($timearray['year'] < 1980) {
 854              $timearray['year']    = 1980;
 855              $timearray['mon']     = 1;
 856              $timearray['mday']    = 1;
 857              $timearray['hours']   = 0;
 858              $timearray['minutes'] = 0;
 859              $timearray['seconds'] = 0;
 860          }
 861          return (($timearray['year'] - 1980) << 25) |
 862          ($timearray['mon'] << 21) |
 863          ($timearray['mday'] << 16) |
 864          ($timearray['hours'] << 11) |
 865          ($timearray['minutes'] << 5) |
 866          ($timearray['seconds'] >> 1);
 867      }
 868  
 869      /**
 870       * Create a UNIX timestamp from a DOS timestamp
 871       *
 872       * @param $mdate
 873       * @param $mtime
 874       * @return int
 875       */
 876      protected function makeUnixTime($mdate = null, $mtime = null)
 877      {
 878          if ($mdate && $mtime) {
 879              $year = (($mdate & 0xFE00) >> 9) + 1980;
 880              $month = ($mdate & 0x01E0) >> 5;
 881              $day = $mdate & 0x001F;
 882  
 883              $hour = ($mtime & 0xF800) >> 11;
 884              $minute = ($mtime & 0x07E0) >> 5;
 885              $seconde = ($mtime & 0x001F) << 1;
 886  
 887              $mtime = mktime($hour, $minute, $seconde, $month, $day, $year);
 888          } else {
 889              $mtime = time();
 890          }
 891  
 892          return $mtime;
 893      }
 894  
 895      /**
 896       * Returns a local file header for the given data
 897       *
 898       * @param int $offset location of the local header
 899       * @param int $ts unix timestamp
 900       * @param int $crc CRC32 checksum of the uncompressed data
 901       * @param int $len length of the uncompressed data
 902       * @param int $clen length of the compressed data
 903       * @param string $name file name
 904       * @param boolean|null $comp if compression is used, if null it's determined from $len != $clen
 905       * @return string
 906       */
 907      protected function makeCentralFileRecord($offset, $ts, $crc, $len, $clen, $name, $comp = null)
 908      {
 909          if(is_null($comp)) $comp = $len != $clen;
 910          $comp = $comp ? 8 : 0;
 911          $dtime = dechex($this->makeDosTime($ts));
 912  
 913          list($name, $extra) = $this->encodeFilename($name);
 914  
 915          $header = "\x50\x4b\x01\x02"; // central file header signature
 916          $header .= pack('v', 14); // version made by - VFAT
 917          $header .= pack('v', 20); // version needed to extract - 2.0
 918          $header .= pack('v', 0); // general purpose flag - no flags set
 919          $header .= pack('v', $comp); // compression method - deflate|none
 920          $header .= pack(
 921              'H*',
 922              $dtime[6] . $dtime[7] .
 923              $dtime[4] . $dtime[5] .
 924              $dtime[2] . $dtime[3] .
 925              $dtime[0] . $dtime[1]
 926          ); //  last mod file time and date
 927          $header .= pack('V', $crc); // crc-32
 928          $header .= pack('V', $clen); // compressed size
 929          $header .= pack('V', $len); // uncompressed size
 930          $header .= pack('v', strlen($name)); // file name length
 931          $header .= pack('v', strlen($extra)); // extra field length
 932          $header .= pack('v', 0); // file comment length
 933          $header .= pack('v', 0); // disk number start
 934          $header .= pack('v', 0); // internal file attributes
 935          $header .= pack('V', 0); // external file attributes  @todo was 0x32!?
 936          $header .= pack('V', $offset); // relative offset of local header
 937          $header .= $name; // file name
 938          $header .= $extra; // extra (utf-8 filename)
 939  
 940          return $header;
 941      }
 942  
 943      /**
 944       * Returns a local file header for the given data
 945       *
 946       * @param int $ts unix timestamp
 947       * @param int $crc CRC32 checksum of the uncompressed data
 948       * @param int $len length of the uncompressed data
 949       * @param int $clen length of the compressed data
 950       * @param string $name file name
 951       * @param boolean|null $comp if compression is used, if null it's determined from $len != $clen
 952       * @return string
 953       */
 954      protected function makeLocalFileHeader($ts, $crc, $len, $clen, $name, $comp = null)
 955      {
 956          if(is_null($comp)) $comp = $len != $clen;
 957          $comp = $comp ? 8 : 0;
 958          $dtime = dechex($this->makeDosTime($ts));
 959  
 960          list($name, $extra) = $this->encodeFilename($name);
 961  
 962          $header = "\x50\x4b\x03\x04"; //  local file header signature
 963          $header .= pack('v', 20); // version needed to extract - 2.0
 964          $header .= pack('v', 0); // general purpose flag - no flags set
 965          $header .= pack('v', $comp); // compression method - deflate|none
 966          $header .= pack(
 967              'H*',
 968              $dtime[6] . $dtime[7] .
 969              $dtime[4] . $dtime[5] .
 970              $dtime[2] . $dtime[3] .
 971              $dtime[0] . $dtime[1]
 972          ); //  last mod file time and date
 973          $header .= pack('V', $crc); // crc-32
 974          $header .= pack('V', $clen); // compressed size
 975          $header .= pack('V', $len); // uncompressed size
 976          $header .= pack('v', strlen($name)); // file name length
 977          $header .= pack('v', strlen($extra)); // extra field length
 978          $header .= $name; // file name
 979          $header .= $extra; // extra (utf-8 filename)
 980          return $header;
 981      }
 982  
 983      /**
 984       * Returns only a part of the local file header containing the CRC, size and compressed size.
 985       * Used to update these fields for an already written header.
 986       *
 987       * @param int $crc CRC32 checksum of the uncompressed data
 988       * @param int $len length of the uncompressed data
 989       * @param int $clen length of the compressed data
 990       * @return string
 991       */
 992      protected function makeCrcAndSize($crc, $len, $clen) {
 993          $header  = pack('V', $crc); // crc-32
 994          $header .= pack('V', $clen); // compressed size
 995          $header .= pack('V', $len); // uncompressed size
 996          return $header;
 997      }
 998  
 999      /**
1000       * Returns an allowed filename and an extra field header
1001       *
1002       * When encoding stuff outside the 7bit ASCII range it needs to be placed in a separate
1003       * extra field
1004       *
1005       * @param $original
1006       * @return array($filename, $extra)
1007       */
1008      protected function encodeFilename($original)
1009      {
1010          $cp437 = $this->utf8ToCp($original);
1011          if ($cp437 === $original) {
1012              return array($original, '');
1013          }
1014  
1015          $extra = pack(
1016              'vvCV',
1017              0x7075, // tag
1018              strlen($original) + 5, // length of file + version + crc
1019              1, // version
1020              crc32($original) // crc
1021          );
1022          $extra .= $original;
1023  
1024          return array($cp437, $extra);
1025      }
1026  }