Adding Zip-class
authorTim Düsterhus <timwolla@arcor.de>
Thu, 10 May 2012 19:11:28 +0000 (21:11 +0200)
committerTim Düsterhus <timwolla@arcor.de>
Thu, 10 May 2012 19:11:28 +0000 (21:11 +0200)
Zip reads and extracts Zip files.

wcfsetup/install/files/lib/system/io/Zip.class.php [new file with mode: 0644]

diff --git a/wcfsetup/install/files/lib/system/io/Zip.class.php b/wcfsetup/install/files/lib/system/io/Zip.class.php
new file mode 100644 (file)
index 0000000..26aa7b8
--- /dev/null
@@ -0,0 +1,289 @@
+<?php
+namespace wcf\system\io;
+use wcf\system\exception\SystemException;
+use wcf\util\FileUtil;
+
+/**
+ * Reads .zip-Files
+ * 
+ * @author     Tim Düsterhus
+ * @copyright  2012 Tim Düsterhus
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.io
+ * @category   Community Framework
+ */
+class Zip extends File {
+       const LOCAL_FILE_SIGNATURE = "\x50\x4b\x03\x04";
+       const CENTRAL_DIRECTORY_SIGNATURE = "\x50\x4b\x01\x02";
+       const EOF_SIGNATURE = "\x50\x4b\x05\x06";
+       
+       /**
+        * @see \wcf\system\io\File
+        */
+       public function __construct($filename) {
+               parent::__construct($filename, 'rb');
+       }
+       
+       /**
+        * Returns the offset for the given file and false of failure.
+        * 
+        * @param       string  $filename       file to search
+        * @return      mixed
+        */
+       public function getOffsetByFilename($filename) {
+               $this->jumpToCentralDirectory();
+               $centralDirectory = $this->readCentralDirectory();
+               
+               if (isset($centralDirectory['files'][$filename])) return $centralDirectory['files'][$filename]['offset'];
+               return false;
+       }
+       
+       /**
+        * Extracts all files to the given destination.
+        * The directory-structure inside the .zip is preserved.
+        * 
+        * @param       string  $destination    where to extract
+        */
+       public function extractAll($destination) {
+               $destination = FileUtil::addTrailingSlash($destination);
+               $this->seek(0);
+               
+               while ($this->isFile()) {
+                       $offset = $this->tell();
+                       $file = $this->readFile();
+                       $filename = $file['header']['filename'];
+                       $this->extract($offset, $destination.dirname($filename));
+               }
+       }
+       
+       /**
+        * Extracts the file at the given offset to a string.
+        * 
+        * @param       integer         $offset         The offset to extract
+        * @return      string
+        */
+       public function extractToString($offset) {
+               $file = $this->readFile($offset);
+               
+               return $file['content'];
+       }
+       
+       /**
+        * Extracts the file at the given offset to the given destination.
+        * The directory-structure inside the .zip is ignored.
+        *
+        * @param       integer         $offset         The offset to extract
+        * @param       string          $destination    Where to extract
+        * @param       string          $filename       The filename. When not given the original filename is used
+        */
+       public function extract($offset, $destination, $filename = null) {
+               $file = $this->readFile($offset);
+               $filename = ($filename ?: $file['header']['filename']);
+               
+               FileUtil::makePath($destination);
+               $targetFile = new File(FileUtil::addTrailingSlash($destination).basename($filename));
+               $targetFile->write($file['content'], strlen($file['content']));
+               $targetFile->close();
+               
+               if (FileUtil::isApacheModule() || !@$targetFile->is_writable()) {
+                       @$targetFile->chmod(0777);
+               }
+               else {
+                       @$targetFile->chmod(0755);
+               }
+               
+               if ($file['header']['x-timestamp']) {
+                       @$targetFile->touch($file['header']['x-timestamp']);
+               }
+               
+               // check filesize
+               if (filesize(FileUtil::addTrailingSlash($destination).basename($filename)) != $file['header']['uncompressedSize']) {
+                       throw new SystemException("Could not unzip file '".$file['header']['filename']."' to '".FileUtil::addTrailingSlash($destination).basename($filename)."'. Maybe disk quota exceeded in folder '".$destination."'.");
+               }
+       }
+       
+       /**
+        * Moves the file-pointer to the beginning of the Central Directory.
+        */
+       public function jumpToCentralDirectory() {
+               $this->seek(0);
+               while ($this->isFile()) {
+                       $this->skipFile();
+                       $offset = $this->tell();
+               }
+               
+               if ($this->read(4) !== self::CENTRAL_DIRECTORY_SIGNATURE) throw new SystemException('Unable to locate central directory');
+               $this->seek($offset);
+       }
+       
+       /**
+        * Reads the Central Directory and returns it.
+        * 
+        * @param       integer         $offset         where to start reading
+        * @return      array
+        */
+       public function readCentralDirectory($offset = null) {
+               if ($offset === null) $offset = $this->tell();
+               
+               $this->seek($offset);
+               // check signature
+               if ($this->read(4) !== self::CENTRAL_DIRECTORY_SIGNATURE) {
+                       throw new SystemException('Invalid offset passed to readCentralDirectory');
+               }
+               $this->seek($offset);
+               
+               $files = array();
+               while ($this->read(4) === self::CENTRAL_DIRECTORY_SIGNATURE) {
+                       $data = unpack('vversion/vminVersion/vgeneralPurposeBit/vcompression/vmtime/vmdate', $this->read(12));
+                       // calculate timestamp
+                       $second = ($data['mtime'] & 31 /* 5 bits */) * 2;
+                       $minute = ($data['mtime'] >> 5) & 63 /* 6 bits */;
+                       $hour = ($data['mtime'] >> 11) & 31 /* 5 bits */;
+                       $day = $data['mdate'] & 31 /* 5 bits */;
+                       $month = ($data['mdate'] >> 5) & 15 /* 4 bits */;
+                       $year = (($data['mdate'] >> 9) & 127 /* 7 bits */) + 1980;
+                       $data['x-timestamp'] = mktime($hour, $minute, $second, $month, $day, $year);
+                       
+                       $data += unpack('Vcrc32/VcompressedSize/VuncompressedSize/vfilenameLength/vextraFieldLength/vfileCommentLength/vdiskNo/vinternalAttr/vexternalAttr', $this->read(26));
+                       $data['offset'] = $this->readAndUnpack(4, 'v');
+                       $data['filename'] = $this->read($data['filenameLength']);
+                       
+                       // read extraField
+                       if ($data['extraFieldLength'] > 0) $data['extraField'] = $this->read($data['extraFieldLength']);
+                       else $data['extraField'] = '';
+                       // read filecomment
+                       if ($data['fileCommentLength'] > 0) $data['fileComment'] = $this->read($data['fileCommentLength']);
+                       else $data['fileComment'] = '';
+                       
+                       $files[$data['filename']] = $data;
+               }
+               $this->seek($this->tell() - 4);
+               $size = $this->tell() - $offset;
+               
+               if ($this->read(4) !== self::EOF_SIGNATURE) throw new SystemException('Could not find the end of Central Directory');
+               
+               $eof = unpack('vdiskNo/vdiskWithCentralDirectory/vdiskEntries/vtotalEntries/vcentralDirectorySize', $this->read(12));
+               // check size of Central Directory
+               if ($size !== $eof['centralDirectorySize']) throw new SystemException('Central Directory size does not match');
+               $eof += unpack('vcentralDirectoryOffset/vcommentLength', $this->read(6));
+               
+               // read comment
+               if ($eof['commentLength'] > 0) $eof['comment'] = $this->read($eof['commentLength']);
+               else $eof['comment'] = '';
+               
+               return array('files' => $files, 'eof' => $eof);
+       }
+       
+       /**
+        * Checks whether the next record is a file.
+        * This does not change the position of the file-pointer.
+        * 
+        * @param       integer         $offset         where to start reading
+        * @return      boolean
+        */
+       public function isFile($offset = null) {
+               if ($offset === null) $offset = $this->tell();
+               
+               $oldOffset = $this->tell();
+               $this->seek($offset);
+               // check signature
+               $result = $this->read(4) === self::LOCAL_FILE_SIGNATURE;
+               
+               $this->seek($oldOffset);
+               return $result;
+       }
+       
+       /**
+        * Moves the file-pointer right after this file.
+        * 
+        * @param       integer         $offset         where to start reading
+        */
+       public function skipFile($offset = null) {
+               if ($offset === null) $offset = $this->tell();
+               
+               $this->seek($offset);
+               // check signature
+               if ($this->read(4) !== self::LOCAL_FILE_SIGNATURE) {
+                       throw new SystemException('Invalid offset passed to skipFile');
+               }
+               
+               // skip unneccessary header
+               $this->seek($offset + 18);
+               // read compressed filesize
+               $compressedSize = $this->readAndUnpack(4, 'V');
+               $this->read(4);
+               // read length of some fields
+               $filenameLength = $this->readAndUnpack(2, 'v');
+               $extraFieldLength = $this->readAndUnpack(2, 'v');
+               
+               // skip file
+               $this->seek($offset + 30 + $compressedSize + $filenameLength + $extraFieldLength);
+       }
+       
+       /**
+        * Reads a file and returns it.
+        *
+        * @param       integer         $offset         where to start reading
+        * @return      array
+        */
+       public function readFile($offset = null) {
+               if ($offset === null) $offset = $this->tell();
+               
+               $this->seek($offset);
+               // check signature
+               if ($this->read(4) !== self::LOCAL_FILE_SIGNATURE) {
+                       throw new SystemException('Invalid offset passed to readFile');
+               }
+               
+               // read headers
+               $header = array();
+               $header = unpack('vminVersion/vgeneralPurposeBit/vcompression/vmtime/vmdate', $this->read(10));
+               if ($header['generalPurposeBit'] & 7 /* 3rd bit */) throw new SystemException('Data Descriptors are not supported'); 
+               $second = ($header['mtime'] & 31 /* 5 bits */) * 2;
+               $minute = ($header['mtime'] >> 5) & 63 /* 6 bits */;
+               $hour = ($header['mtime'] >> 11) & 31 /* 5 bits */;
+               $day = $header['mdate'] & 31 /* 5 bits */;
+               $month = ($header['mdate'] >> 5) & 15 /* 4 bits */;
+               $year = (($header['mdate'] >> 9) & 127 /* 7 bits */) + 1980;
+               $header['x-timestamp'] = mktime($hour, $minute, $second, $month, $day, $year);
+               $header += unpack('Vcrc32/VcompressedSize/VuncompressedSize/vfilenameLength/vextraFieldLength', $this->read(16));
+               
+               // read filename
+               $header['filename'] = $this->read($header['filenameLength']);
+               // read extra field
+               if ($header['extraFieldLength'] > 0) $header['extraField'] = $this->read($header['extraFieldLength']);
+               else $header['extraField'] = '';
+               
+               // read contents
+               $content = $this->read($header['compressedSize']);
+               
+               // uncompress file
+               switch ($header['compression']) {
+                       case 8:
+                               $content = gzinflate($content);
+                       case 0:
+                       break;
+                       default:
+                               throw new SystemException('Compression '.$header['compression'].' is not supported');
+               }
+               
+               // check crc32
+               if (crc32($content) != $header['crc32']) throw new SystemException('Checksum does not match');
+               
+               return array('header' => $header, 'content' => $content);
+       }
+       
+       /**
+        * Reads in the specified number of bytes and unpacks them.
+        * 
+        * @param       integer         $length         Number of bytes to read
+        * @param       string          $type           Which type are the bytes of
+        * @return      mixed
+        */
+       protected function readAndUnpack($length, $type) {
+               $data = unpack($type, $this->read($length));
+               
+               return $data[1];
+       }
+}