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$ 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 * 78 * @see File_Archive_Reader::next() 79 */ 80 function next() 81 { 82 if ($this->seekToEnd > 0) { 83 return false; 84 } 85 86 //Skip the data and the footer if they haven't been uncompressed 87 if ($this->header !== null && $this->data === null) { 88 $toSkip = $this->header['CLen']; 89 $error = $this->source->skip($toSkip); 90 if (PEAR::isError($error)) { 91 return $error; 92 } 93 } 94 95 $this->offset = 0; 96 $this->data = null; 97 98 //Read the header 99 $header = $this->source->getData(4); 100 // Handle PK00PK archives 101 if ($header == "\x50\x4b\x30\x30") { //PK00 102 $header = $this->source->getData(4); 103 } 104 // Sometimes this header is used to tag the data descriptor section 105 if($header == "\x50\x4b\x07\x08") { 106 // Read out the data descriptor (always 12 bytes) 107 $this->source->getData(12); 108 109 // Get a new header from the file 110 $header = $this->source->getData(4); 111 } 112 if (PEAR::isError($header)) { 113 return $header; 114 } 115 if ($header == "\x50\x4b\x03\x04") { 116 //New entry 117 $header = $this->source->getData(26); 118 if (PEAR::isError($header)) { 119 return $header; 120 } 121 $this->header = unpack( 122 "vVersion/vFlag/vMethod/vTime/vDate/VCRC/VCLen/VNLen/vFile/vExtra", 123 $header); 124 125 //Check the compression method 126 if ($this->header['Method'] != 0 && 127 $this->header['Method'] != 8 && 128 $this->header['Method'] != 12) { 129 return PEAR::raiseError("File_Archive_Reader_Zip doesn't ". 130 "handle compression method {$this->header['Method']}"); 131 } 132 if ($this->header['Flag'] & 1) { 133 return PEAR::raiseError("File_Archive_Reader_Zip doesn't ". 134 "handle encrypted files"); 135 } 136 if ($this->header['Flag'] & 8) { 137 if ($this->centralDirectory === null) { 138 $this->readCentralDirectory(); 139 } 140 $centralDirEntry = $this->centralDirectory[count($this->files)]; 141 142 $this->header['CRC'] = $centralDirEntry['CRC']; 143 $this->header['CLen'] = $centralDirEntry['CLen']; 144 $this->header['NLen'] = $centralDirEntry['NLen']; 145 } 146 if ($this->header['Flag'] & 32) { 147 return PEAR::raiseError("File_Archive_Reader_Zip doesn't ". 148 "handle compressed patched data"); 149 } 150 if ($this->header['Flag'] & 64) { 151 return PEAR::raiseError("File_Archive_Reader_Zip doesn't ". 152 "handle strong encrypted files"); 153 } 154 155 $this->currentStat = array( 156 7=>$this->header['NLen'], 157 9=>mktime( 158 ($this->header['Time'] & 0xF800) >> 11, //hour 159 ($this->header['Time'] & 0x07E0) >> 5, //minute 160 ($this->header['Time'] & 0x001F) >> 1, //second 161 ($this->header['Date'] & 0x01E0) >> 5, //month 162 ($this->header['Date'] & 0x001F) , //day 163 (($this->header['Date'] & 0xFE00) >> 9) + 1980 //year 164 ) 165 ); 166 $this->currentStat['size'] = $this->currentStat[7]; 167 $this->currentStat['mtime'] = $this->currentStat[9]; 168 169 $this->currentFilename = $this->source->getData($this->header['File']); 170 171 $error = $this->source->skip($this->header['Extra']); 172 if (PEAR::isError($error)) { 173 return $error; 174 } 175 176 $this->files[] = array('name' => $this->currentFilename, 177 'stat' => $this->currentStat, 178 'CRC' => $this->header['CRC'], 179 'CLen' => $this->header['CLen'] 180 ); 181 return true; 182 } else { 183 //Begining of central area 184 $this->seekToEnd = 4; 185 $this->currentFilename = null; 186 return false; 187 } 188 } 189 190 /** 191 * @see File_Archive_Reader::getData() 192 */ 193 function getData($length = -1) 194 { 195 if ($this->offset >= $this->currentStat[7]) { 196 return null; 197 } 198 199 if ($length>=0) { 200 $actualLength = min($length, $this->currentStat[7]-$this->offset); 201 } else { 202 $actualLength = $this->currentStat[7]-$this->offset; 203 } 204 205 $error = $this->uncompressData(); 206 if (PEAR::isError($error)) { 207 return $error; 208 } 209 $result = substr($this->data, $this->offset, $actualLength); 210 $this->offset += $actualLength; 211 return $result; 212 } 213 /** 214 * @see File_Archive_Reader::skip() 215 */ 216 function skip($length = -1) 217 { 218 $before = $this->offset; 219 if ($length == -1) { 220 $this->offset = $this->currentStat[7]; 221 } else { 222 $this->offset = min($this->offset + $length, $this->currentStat[7]); 223 } 224 return $this->offset - $before; 225 } 226 /** 227 * @see File_Archive_Reader::rewind() 228 */ 229 function rewind($length = -1) 230 { 231 $before = $this->offset; 232 if ($length == -1) { 233 $this->offset = 0; 234 } else { 235 $this->offset = min(0, $this->offset - $length); 236 } 237 return $before - $this->offset; 238 } 239 /** 240 * @see File_Archive_Reader::tell() 241 */ 242 function tell() 243 { 244 return $this->offset; 245 } 246 247 function uncompressData() 248 { 249 if ($this->data !== null) 250 return; 251 252 $this->data = $this->source->getData($this->header['CLen']); 253 if (PEAR::isError($this->data)) { 254 return $this->data; 255 } 256 if ($this->header['Method'] == 8) { 257 $this->data = gzinflate($this->data); 258 } 259 if ($this->header['Method'] == 12) { 260 $this->data = bzdecompress($this->data); 261 } 262 263 if (crc32($this->data) != ($this->header['CRC'] & 0xFFFFFFFF)) { 264 return PEAR::raiseError("Zip archive: CRC fails on entry ". 265 $this->currentFilename); 266 } 267 } 268 269 /** 270 * @see File_Archive_Reader::makeWriterRemoveFiles() 271 */ 272 function makeWriterRemoveFiles($pred) 273 { 274 require_once "File/Archive/Writer/Zip.php"; 275 276 $blocks = array(); 277 $seek = null; 278 $gap = 0; 279 if ($this->currentFilename !== null && $pred->isTrue($this)) { 280 $seek = 30 + $this->header['File'] + $this->header['Extra'] + $this->header['CLen']; 281 $blocks[] = $seek; //Remove this file 282 array_pop($this->files); 283 } 284 285 while (($error = $this->next()) === true) { 286 $size = 30 + $this->header['File'] + $this->header['Extra'] + $this->header['CLen']; 287 if (substr($this->getFilename(), -1) == '/' || $pred->isTrue($this)) { 288 array_pop($this->files); 289 if ($seek === null) { 290 $seek = $size; 291 $blocks[] = $size; 292 } else if ($gap > 0) { 293 $blocks[] = $gap; //Don't remove the files between the gap 294 $blocks[] = $size; 295 $seek += $size; 296 } else { 297 $blocks[count($blocks)-1] += $size; //Also remove this file 298 $seek += $size; 299 } 300 $gap = 0; 301 } else { 302 if ($seek !== null) { 303 $seek += $size; 304 $gap += $size; 305 } 306 } 307 } 308 if (PEAR::isError($error)) { 309 return $error; 310 } 311 312 if ($seek === null) { 313 $seek = 4; 314 } else { 315 $seek += 4; 316 if ($gap == 0) { 317 array_pop($blocks); 318 } else { 319 $blocks[] = $gap; 320 } 321 } 322 323 $writer = new File_Archive_Writer_Zip(null, 324 $this->source->makeWriterRemoveBlocks($blocks, -$seek) 325 ); 326 if (PEAR::isError($writer)) { 327 return $writer; 328 } 329 330 foreach ($this->files as $file) { 331 $writer->alreadyWrittenFile($file['name'], $file['stat'], $file['CRC'], $file['CLen']); 332 } 333 334 $this->close(); 335 return $writer; 336 } 337 338 /** 339 * @see File_Archive_Reader::makeWriterRemoveBlocks() 340 */ 341 function makeWriterRemoveBlocks($blocks, $seek = 0) 342 { 343 if ($this->currentFilename === null) { 344 return PEAR::raiseError('No file selected'); 345 } 346 347 $keep = false; 348 349 $this->uncompressData(); 350 $newData = substr($this->data, 0, $this->offset + $seek); 351 $this->data = substr($this->data, $this->offset + $seek); 352 foreach ($blocks as $length) { 353 if ($keep) { 354 $newData .= substr($this->data, 0, $length); 355 } 356 $this->data = substr($this->data, $length); 357 $keep = !$keep; 358 } 359 if ($keep) { 360 $newData .= $this->data; 361 } 362 363 $filename = $this->currentFilename; 364 $stat = $this->currentStat; 365 366 $writer = $this->makeWriterRemove(); 367 if (PEAR::isError($writer)) { 368 return $writer; 369 } 370 371 unset($stat[7]); 372 $stat[9] = $stat['mtime'] = time(); 373 $writer->newFile($filename, $stat); 374 $writer->writeData($newData); 375 return $writer; 376 } 377 378 /** 379 * @see File_Archive_Reader::makeAppendWriter 380 */ 381 function makeAppendWriter() 382 { 383 require_once "File/Archive/Writer/Zip.php"; 384 385 while (($error = $this->next()) === true) { } 386 if (PEAR::isError($error)) { 387 $this->close(); 388 return $error; 389 } 390 391 $writer = new File_Archive_Writer_Zip(null, 392 $this->source->makeWriterRemoveBlocks(array(), -4) 393 ); 394 395 foreach ($this->files as $file) { 396 $writer->alreadyWrittenFile($file['name'], $file['stat'], $file['CRC'], $file['CLen']); 397 } 398 399 $this->close(); 400 return $writer; 401 } 402 403 /** 404 * This function seeks to the start of the [end of central directory] field, 405 * just after the \x50\x4b\x05\x06 signature and returns the number of bytes 406 * skipped 407 * 408 * The stream must initially be positioned before the end of central directory 409 */ 410 function seekToEndOfCentralDirectory() 411 { 412 $nbSkipped = $this->source->skip(); 413 414 $nbSkipped -= $this->source->rewind(22) - 4; 415 if ($this->source->getData(4) == "\x50\x4b\x05\x06") { 416 return $nbSkipped; 417 } 418 419 while ($nbSkipped > 0) { 420 421 $nbRewind = $this->source->rewind(min(100, $nbSkipped)); 422 while ($nbRewind >= -4) { 423 if ($nbRewind-- && $this->source->getData(1) == "\x50" && 424 $nbRewind-- && $this->source->getData(1) == "\x4b" && 425 $nbRewind-- && $this->source->getData(1) == "\x05" && 426 $nbRewind-- && $this->source->getData(1) == "\x06") { 427 //We finally found it! 428 return $nbSkipped - $nbRewind; 429 } 430 } 431 $nbSkipped -= $nbRewind; 432 } 433 434 return PEAR::raiseError('End of central directory not found. The file is probably not a zip archive'); 435 } 436 437 /** 438 * This function will fill the central directory variable 439 * and seek back to where it was called 440 */ 441 function readCentralDirectory() 442 { 443 $nbSkipped = $this->seekToEndOfCentralDirectory(); 444 if (PEAR::isError($nbSkipped)) { 445 return $nbSkipped; 446 } 447 448 $this->source->skip(12); 449 $offset = $this->source->getData(4); 450 $nbSkipped += 16; 451 if (PEAR::isError($offset)) { 452 return $offset; 453 } 454 455 $offset = unpack("Vvalue", $offset); 456 $offset = $offset['value']; 457 458 $current = $this->source->tell(); 459 $nbSkipped -= $this->source->rewind($current - $offset); 460 461 //Now we are the right pos to read the central directory 462 $this->centralDirectory = array(); 463 while ($this->source->getData(4) == "\x50\x4b\x01\x02") { 464 $this->source->skip(12); 465 $header = $this->source->getData(16); 466 $nbSkipped += 32; 467 468 if (PEAR::isError($header)) { 469 return $header; 470 } 471 472 $header = unpack('VCRC/VCLen/VNLen/vFileLength/vExtraLength', $header); 473 $this->centralDirectory[] = array('CRC' => $header['CRC'], 474 'CLen' => $header['CLen'], 475 'NLen' => $header['NLen']); 476 $nbSkipped += $this->source->skip(14 + $header['FileLength'] + $header['ExtraLength']); 477 } 478 479 $this->source->rewind($nbSkipped+4); 480 } 481} 482?> 483