1<?php 2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 3 4/** 5 * ZIP archive reader 6 * 7 * PHP versions 4 and 5 8 * 9 * This library is free software; you can redistribute it and/or 10 * modify it under the terms of the GNU Lesser General Public 11 * License as published by the Free Software Foundation; either 12 * version 2.1 of the License, or (at your option) any later version. 13 * 14 * This library is distributed in the hope that it will be useful, 15 * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 * Lesser General Public License for more details. 18 * 19 * You should have received a copy of the GNU Lesser General Public 20 * License along with this library; if not, write to the Free Software 21 * Foundation, Inc., 59 Temple Place, Suite 330,Boston,MA 02111-1307 USA 22 * 23 * @category File Formats 24 * @package File_Archive 25 * @author Vincent Lascaux <vincentlascaux@php.net> 26 * @copyright 1997-2005 The PHP Group 27 * @license http://www.gnu.org/copyleft/lesser.html LGPL 28 * @version CVS: $Id: Zip.php,v 1.26 2005/06/19 20:09:58 vincentlascaux Exp $ 29 * @link http://pear.php.net/package/File_Archive 30 */ 31 32require_once "File/Archive/Reader/Archive.php"; 33 34/** 35 * ZIP archive reader 36 * Currently only allows to browse the archive (getData is not available) 37 */ 38class File_Archive_Reader_Zip extends File_Archive_Reader_Archive 39{ 40 var $currentFilename = null; 41 var $currentStat = null; 42 var $header = null; 43 var $offset = 0; 44 var $data = null; 45 var $files = array(); 46 var $seekToEnd = 0; 47 48 var $centralDirectory = null; 49 50 /** 51 * @see File_Archive_Reader::close() 52 */ 53 function close() 54 { 55 $this->currentFilename = null; 56 $this->currentStat = null; 57 $this->compLength = 0; 58 $this->data = null; 59 $this->seekToEnd = 0; 60 $this->files = array(); 61 $this->centralDirectory = null; 62 63 return parent::close(); 64 } 65 66 /** 67 * @see File_Archive_Reader::getFilename() 68 */ 69 function getFilename() { return $this->currentFilename; } 70 /** 71 * @see File_Archive_Reader::getStat() 72 */ 73 function getStat() { return $this->currentStat; } 74 75 /** 76 * Go to next entry in ZIP archive 77 * This function may stop on a folder, so it does not comply to the 78 * File_Archive_Reader::next specs 79 * 80 * @see File_Archive_Reader::next() 81 */ 82 function nextWithFolders() 83 { 84 if ($this->seekToEnd > 0) { 85 return false; 86 } 87 88 //Skip the data and the footer if they haven't been uncompressed 89 if ($this->header !== null && $this->data === null) { 90 $toSkip = $this->header['CLen']; 91 $error = $this->source->skip($toSkip); 92 if (PEAR::isError($error)) { 93 return $error; 94 } 95 } 96 97 $this->offset = 0; 98 $this->data = null; 99 100 //Read the header 101 $header = $this->source->getData(4); 102 if (PEAR::isError($header)) { 103 return $header; 104 } 105 if ($header == "\x50\x4b\x03\x04") { 106 //New entry 107 $header = $this->source->getData(26); 108 if (PEAR::isError($header)) { 109 return $header; 110 } 111 $this->header = unpack( 112 "vVersion/vFlag/vMethod/vTime/vDate/VCRC/VCLen/VNLen/vFile/vExtra", 113 $header); 114 115 //Check the compression method 116 if ($this->header['Method'] != 0 && 117 $this->header['Method'] != 8 && 118 $this->header['Method'] != 12) { 119 return PEAR::raiseError("File_Archive_Reader_Zip doesn't ". 120 "handle compression method {$this->header['Method']}"); 121 } 122 if ($this->header['Flag'] & 1) { 123 return PEAR::raiseError("File_Archive_Reader_Zip doesn't ". 124 "handle encrypted files"); 125 } 126 if ($this->header['Flag'] & 8) { 127 if ($this->centralDirectory === null) { 128 $this->readCentralDirectory(); 129 } 130 $centralDirEntry = $this->centralDirectory[count($this->files)]; 131 132 $this->header['CRC'] = $centralDirEntry['CRC']; 133 $this->header['CLen'] = $centralDirEntry['CLen']; 134 $this->header['NLen'] = $centralDirEntry['NLen']; 135 } 136 if ($this->header['Flag'] & 32) { 137 return PEAR::raiseError("File_Archive_Reader_Zip doesn't ". 138 "handle compressed patched data"); 139 } 140 if ($this->header['Flag'] & 64) { 141 return PEAR::raiseError("File_Archive_Reader_Zip doesn't ". 142 "handle strong encrypted files"); 143 } 144 145 $this->currentStat = array( 146 7=>$this->header['NLen'], 147 9=>mktime( 148 ($this->header['Time'] & 0xF800) >> 11, //hour 149 ($this->header['Time'] & 0x07E0) >> 5, //minute 150 ($this->header['Time'] & 0x001F) >> 1, //second 151 ($this->header['Date'] & 0x01E0) >> 5, //month 152 ($this->header['Date'] & 0x001F) , //day 153 (($this->header['Date'] & 0xFE00) >> 9) + 1980 //year 154 ) 155 ); 156 $this->currentStat['size'] = $this->currentStat[7]; 157 $this->currentStat['mtime'] = $this->currentStat[9]; 158 159 $this->currentFilename = $this->source->getData($this->header['File']); 160 161 $error = $this->source->skip($this->header['Extra']); 162 if (PEAR::isError($error)) { 163 return $error; 164 } 165 166 $this->files[] = array('name' => $this->currentFilename, 167 'stat' => $this->currentStat, 168 'CRC' => $this->header['CRC'], 169 'CLen' => $this->header['CLen'] 170 ); 171 172 return true; 173 } else { 174 //Begining of central area 175 $this->seekToEnd = 4; 176 $this->currentFilename = null; 177 return false; 178 } 179 } 180 /** 181 * Go to next file entry in ZIP archive 182 * This function will not stop on a folder entry 183 * @see File_Archive_Reader::next() 184 */ 185 function next() 186 { 187 if (!parent::next()) { 188 return false; 189 } 190 191 do { 192 $result = $this->nextWithFolders(); 193 if ($result !== true) { 194 return $result; 195 } 196 } while (substr($this->getFilename(), -1) == '/'); 197 198 return true; 199 } 200 201 /** 202 * @see File_Archive_Reader::getData() 203 */ 204 function getData($length = -1) 205 { 206 if ($this->offset >= $this->currentStat[7]) { 207 return null; 208 } 209 210 if ($length>=0) { 211 $actualLength = min($length, $this->currentStat[7]-$this->offset); 212 } else { 213 $actualLength = $this->currentStat[7]-$this->offset; 214 } 215 216 $error = $this->uncompressData(); 217 if (PEAR::isError($error)) { 218 return $error; 219 } 220 $result = substr($this->data, $this->offset, $actualLength); 221 $this->offset += $actualLength; 222 return $result; 223 } 224 /** 225 * @see File_Archive_Reader::skip() 226 */ 227 function skip($length = -1) 228 { 229 $before = $this->offset; 230 if ($length == -1) { 231 $this->offset = $this->currentStat[7]; 232 } else { 233 $this->offset = min($this->offset + $length, $this->currentStat[7]); 234 } 235 return $this->offset - $before; 236 } 237 /** 238 * @see File_Archive_Reader::rewind() 239 */ 240 function rewind($length = -1) 241 { 242 $before = $this->offset; 243 if ($length == -1) { 244 $this->offset = 0; 245 } else { 246 $this->offset = min(0, $this->offset - $length); 247 } 248 return $before - $this->offset; 249 } 250 /** 251 * @see File_Archive_Reader::tell() 252 */ 253 function tell() 254 { 255 return $this->offset; 256 } 257 258 function uncompressData() 259 { 260 if ($this->data !== null) 261 return; 262 263 $this->data = $this->source->getData($this->header['CLen']); 264 if (PEAR::isError($this->data)) { 265 return $this->data; 266 } 267 if ($this->header['Method'] == 8) { 268 $this->data = gzinflate($this->data); 269 } 270 if ($this->header['Method'] == 12) { 271 $this->data = bzdecompress($this->data); 272 } 273 274 if (crc32($this->data) != $this->header['CRC']) { 275 return PEAR::raiseError("Zip archive: CRC fails on entry ". 276 $this->currentFilename); 277 } 278 } 279 280 /** 281 * @see File_Archive_Reader::makeWriterRemoveFiles() 282 */ 283 function makeWriterRemoveFiles($pred) 284 { 285 require_once "File/Archive/Writer/Zip.php"; 286 287 $blocks = array(); 288 $seek = null; 289 $gap = 0; 290 if ($this->currentFilename !== null && $pred->isTrue($this)) { 291 $seek = 30 + $this->header['File'] + $this->header['Extra'] + $this->header['CLen']; 292 $blocks[] = $seek; //Remove this file 293 array_pop($this->files); 294 } 295 296 while (($error = $this->nextWithFolders()) === true) { 297 $size = 30 + $this->header['File'] + $this->header['Extra'] + $this->header['CLen']; 298 if (substr($this->getFilename(), -1) == '/' || $pred->isTrue($this)) { 299 array_pop($this->files); 300 if ($seek === null) { 301 $seek = $size; 302 $blocks[] = $size; 303 } else if ($gap > 0) { 304 $blocks[] = $gap; //Don't remove the files between the gap 305 $blocks[] = $size; 306 $seek += $size; 307 } else { 308 $blocks[count($blocks)-1] += $size; //Also remove this file 309 $seek += $size; 310 } 311 $gap = 0; 312 } else { 313 if ($seek !== null) { 314 $seek += $size; 315 $gap += $size; 316 } 317 } 318 } 319 if (PEAR::isError($error)) { 320 return $error; 321 } 322 323 if ($seek === null) { 324 $seek = 4; 325 } else { 326 $seek += 4; 327 if ($gap == 0) { 328 array_pop($blocks); 329 } else { 330 $blocks[] = $gap; 331 } 332 } 333 334 $writer = new File_Archive_Writer_Zip(null, 335 $this->source->makeWriterRemoveBlocks($blocks, -$seek) 336 ); 337 if (PEAR::isError($writer)) { 338 return $writer; 339 } 340 341 foreach ($this->files as $file) { 342 $writer->alreadyWrittenFile($file['name'], $file['stat'], $file['CRC'], $file['CLen']); 343 } 344 345 $this->close(); 346 return $writer; 347 } 348 349 /** 350 * @see File_Archive_Reader::makeWriterRemoveBlocks() 351 */ 352 function makeWriterRemoveBlocks($blocks, $seek = 0) 353 { 354 if ($this->currentFilename === null) { 355 return PEAR::raiseError('No file selected'); 356 } 357 358 $keep = false; 359 360 $this->uncompressData(); 361 $newData = substr($this->data, 0, $this->offset + $seek); 362 $this->data = substr($this->data, $this->offset + $seek); 363 foreach ($blocks as $length) { 364 if ($keep) { 365 $newData .= substr($this->data, 0, $length); 366 } 367 $this->data = substr($this->data, $length); 368 $keep = !$keep; 369 } 370 if ($keep) { 371 $newData .= $this->data; 372 } 373 374 $filename = $this->currentFilename; 375 $stat = $this->currentStat; 376 377 $writer = $this->makeWriterRemove(); 378 if (PEAR::isError($writer)) { 379 return $writer; 380 } 381 382 unset($stat[7]); 383 $stat[9] = $stat['mtime'] = time(); 384 $writer->newFile($filename, $stat); 385 $writer->writeData($newData); 386 return $writer; 387 } 388 389 /** 390 * @see File_Archive_Reader::makeAppendWriter 391 */ 392 function makeAppendWriter() 393 { 394 require_once "File/Archive/Writer/Zip.php"; 395 396 while (($error = $this->next()) === true) { } 397 if (PEAR::isError($error)) { 398 $this->close(); 399 return $error; 400 } 401 402 $writer = new File_Archive_Writer_Zip(null, 403 $this->source->makeWriterRemoveBlocks(array(), -4) 404 ); 405 406 foreach ($this->files as $file) { 407 $writer->alreadyWrittenFile($file['name'], $file['stat'], $file['CRC'], $file['CLen']); 408 } 409 410 $this->close(); 411 return $writer; 412 } 413 414 /** 415 * This function seeks to the start of the [end of central directory] field, 416 * just after the \x50\x4b\x05\x06 signature and returns the number of bytes 417 * skipped 418 * 419 * The stream must initially be positioned before the end of central directory 420 */ 421 function seekToEndOfCentralDirectory() 422 { 423 $nbSkipped = $this->source->skip(); 424 425 $nbSkipped -= $this->source->rewind(22) - 4; 426 if ($this->source->getData(4) == "\x50\x4b\x05\x06") { 427 return $nbSkipped; 428 } 429 430 while ($nbSkipped > 0) { 431 432 $nbRewind = $this->source->rewind(min(100, $nbSkipped)); 433 while ($nbRewind >= -4) { 434 if ($nbRewind-- && $this->source->getData(1) == "\x50" && 435 $nbRewind-- && $this->source->getData(1) == "\x4b" && 436 $nbRewind-- && $this->source->getData(1) == "\x05" && 437 $nbRewind-- && $this->source->getData(1) == "\x06") { 438 //We finally found it! 439 return $nbSkipped - $nbRewind; 440 } 441 } 442 $nbSkipped -= $nbRewind; 443 } 444 445 return PEAR::raiseError('End of central directory not found. The file is probably not a zip archive'); 446 } 447 448 /** 449 * This function will fill the central directory variable 450 * and seek back to where it was called 451 */ 452 function readCentralDirectory() 453 { 454 $nbSkipped = $this->seekToEndOfCentralDirectory(); 455 if (PEAR::isError($nbSkipped)) { 456 return $nbSkipped; 457 } 458 459 $this->source->skip(12); 460 $offset = $this->source->getData(4); 461 $nbSkipped += 16; 462 if (PEAR::isError($offset)) { 463 return $offset; 464 } 465 466 $offset = unpack("Vvalue", $offset); 467 $offset = $offset['value']; 468 469 $current = $this->source->tell(); 470 $nbSkipped -= $this->source->rewind($current - $offset); 471 472 //Now we are the right pos to read the central directory 473 $this->centralDirectory = array(); 474 while ($this->source->getData(4) == "\x50\x4b\x01\x02") { 475 $this->source->skip(12); 476 $header = $this->source->getData(16); 477 $nbSkipped += 32; 478 479 if (PEAR::isError($header)) { 480 return $header; 481 } 482 483 $header = unpack('VCRC/VCLen/VNLen/vFileLength/vExtraLength', $header); 484 $this->centralDirectory[] = array('CRC' => $header['CRC'], 485 'CLen' => $header['CLen'], 486 'NLen' => $header['NLen']); 487 $nbSkipped += $this->source->skip(14 + $header['FileLength'] + $header['ExtraLength']); 488 } 489 490 $this->source->rewind($nbSkipped+4); 491 } 492} 493?>