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