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