1<?php 2/** 3 * Read a file saved in Ar file format 4 * 5 * PHP versions 4 and 5 6 * 7 * This library is free software; you can redistribute it and/or 8 * modify it under the terms of the GNU Lesser General Public 9 * License as published by the Free Software Foundation; either 10 * version 2.1 of the License, or (at your option) any later version. 11 * 12 * This library is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 * Lesser General Public License for more details. 16 * 17 * You should have received a copy of the GNU Lesser General Public 18 * License along with this library; if not, write to the Free Software 19 * Foundation, Inc., 59 Temple Place, Suite 330,Boston,MA 02111-1307 USA 20 * 21 * @category File Formats 22 * @package File_Archive 23 * @author Pablo Fischer <pablo@pablo.com.mx> 24 * @copyright 1997-2005 The PHP Group 25 * @license http://www.gnu.org/copyleft/lesser.html LGPL 26 * @version CVS: $Id: 27 * @link http://pear.php.net/package/File_Archive 28 */ 29 30require_once "File/Archive/Reader/Archive.php"; 31 32/** 33 * Read an Ar archive 34 */ 35class File_Archive_Reader_Ar extends File_Archive_Reader_Archive 36{ 37 /** 38 * @var int The number of files to read to reach the end of the 39 * current ar file 40 * 41 * @access private 42 */ 43 var $_nbBytesLeft = 0; 44 45 /** 46 * @var int The size of the header in number of bytes 47 * The header is not always 60 bytes since it sometimes 48 * contains a long filename 49 * @access private 50 */ 51 var $_header = 0; 52 53 /** 54 * @var boolean Flag set if their is a 1 byte footer after the data 55 * of the current ar file 56 * 57 * @access private 58 */ 59 var $_footer = false; 60 61 /** 62 * @var boolean Flag that has tell us if we have read the header of the 63 * current file 64 * @access private 65 */ 66 var $_alreadyRead = false; 67 68 /** 69 * @var string Name of the file being read 70 * @access private 71 */ 72 var $_currentFilename = null; 73 74 /** 75 * @var string Stat properties of the file being read 76 * It has: name, utime, uid, gid, mode, size and data 77 * @access private 78 */ 79 var $_currentStat = null; 80 81 /** 82 * @see File_Archive_Reader::getFilename() 83 */ 84 function getFilename() 85 { 86 return $this->_currentFilename; 87 } 88 89 /** 90 * @see File_Archive_Reader::close() 91 */ 92 function close() 93 { 94 $this->_currentFilename = null; 95 $this->_currentStat = null; 96 $this->_nbBytesLeft = 0; 97 $this->_header = 0; 98 $this->_footer = false; 99 $this->_alreadyRead = false; 100 return parent::close(); 101 } 102 103 /** 104 * @see File_Archive_Reader::getStat() 105 */ 106 function getStat() 107 { 108 return $this->_currentStat; 109 } 110 111 /** 112 * @see File_Archive_Reader::next() 113 */ 114 function next() 115 { 116 $error = parent::next(); 117 if ($error !== true) { 118 return $error; 119 } 120 121 $this->source->skip( 122 $this->_nbBytesLeft + ($this->_footer ? 1 : 0) 123 ); 124 125 $filename = $this->source->getDataFilename(); 126 127 if (!$this->_alreadyRead) { 128 $header = $this->source->getData(8); 129 if ($header != "!<arch>\n") { 130 return PEAR::raiseError("File {$filename} is not a valid Ar file format (starts with $header)"); 131 } 132 $this->_alreadyRead = true; 133 } 134 135 136 $name = $this->source->getData(16); 137 $mtime = $this->source->getData(12); 138 $uid = $this->source->getData(6); 139 $gid = $this->source->getData(6); 140 $mode = $this->source->getData(8); 141 $size = $this->source->getData(10); 142 $delim = $this->source->getData(2); 143 144 if ($delim === null) { 145 return false; 146 } 147 148 // All files inside should have more than 0 bytes of size 149 if ($size < 0) { 150 return PEAR::raiseError("Files must be at least one byte long"); 151 } 152 153 $this->_footer = ($size % 2 == 1); 154 155 // if the filename starts with a length, then just read the bytes of it 156 if (preg_match("/\#1\/(\d+)/", $name, $matches)) { 157 $this->_header = 60 + $matches[1]; 158 $name = $this->source->getData($matches[1]); 159 $size -= $matches[1]; 160 } else { 161 // strip trailing spaces in name, so we can distinguish spaces in a filename with padding 162 $this->_header = 60; 163 $name = preg_replace ("/\s+$/", "", $name); 164 } 165 166 $this->_nbBytesLeft = $size; 167 if (empty($name) || empty($mtime) || empty($uid) || 168 empty($gid) || empty($mode) || empty($size)) { 169 return PEAR::raiseError("An ar field is empty"); 170 } 171 172 $this->_currentFilename = $this->getStandardURL($name); 173 $this->_currentStat = array( 174 2 => $mode, 175 'mode' => $mode, 176 4 => $uid, 177 'uid' => $uid, 178 5 => $gid, 179 'gid' => $gid, 180 7 => $size, 181 'size' => $size, 182 9 => $mtime, 183 'mtime' => $mtime 184 ); 185 186 return true; 187 } 188 189 /** 190 * @see File_Archive_Reader::getData() 191 */ 192 function getData($length = -1) 193 { 194 if ($length == -1) { 195 $length = $this->_nbBytesLeft; 196 } else { 197 $length = min($length, $this->_nbBytesLeft); 198 } 199 if ($length == 0) { 200 return null; 201 } else { 202 $this->_nbBytesLeft -= $length; 203 $data = $this->source->getData($length); 204 if (PEAR::isError($data)) { 205 return $data; 206 } 207 if (strlen($data) != $length) { 208 return PEAR::raiseError('Unexpected end of Ar archive'); 209 } 210 return $data; 211 } 212 } 213 214 /** 215 * @see File_Archive_Reader::skip 216 */ 217 function skip($length = -1) 218 { 219 if ($length == -1) { 220 $length = $this->_nbBytesLeft; 221 } else { 222 $length = min($length, $this->_nbBytesLeft); 223 } 224 if ($length == 0) { 225 return 0; 226 } else { 227 $this->_nbBytesLeft -= $length; 228 $skipped = $this->source->skip($length); 229 if (PEAR::isError($skipped)) { 230 return $skipped; 231 } 232 if ($skipped != $length) { 233 return PEAR::raiseError('Unexpected end of Ar archive'); 234 } 235 return $skipped; 236 } 237 } 238 239 /** 240 * @see File_Archive_Reader::rewind 241 */ 242 function rewind($length = -1) 243 { 244 if ($length == -1) { 245 $length = $this->_currentStat[7] - $this->_nbBytesLeft; 246 } else { 247 $length = min($length, $this->_currentStat[7] - $this->_nbBytesLeft); 248 } 249 if ($length == 0) { 250 return 0; 251 } else { 252 $rewinded = $this->source->rewind($length); 253 if (!PEAR::isError($rewinded)) { 254 $this->_nbBytesLeft += $rewinded; 255 } 256 return $rewinded; 257 } 258 } 259 260 /** 261 * @see File_Archive_Reader::tell() 262 */ 263 function tell() 264 { 265 return $this->_currentStat[7] - $this->_nbBytesLeft; 266 } 267 268 /** 269 * @see File_Archive_Reader::makeWriterRemoveFiles() 270 */ 271 function makeWriterRemoveFiles($pred) 272 { 273 require_once "File/Archive/Writer/Ar.php"; 274 275 $blocks = array(); 276 $seek = null; 277 $gap = 0; 278 if ($this->_currentFilename !== null && $pred->isTrue($this)) { 279 $seek = $this->_header + $this->_currentStat[7] + ($this->_footer ? 1 : 0); 280 $blocks[] = $seek; //Remove this file 281 } 282 283 while (($error = $this->next()) === true) { 284 $size = $this->_header + $this->_currentStat[7] + ($this->_footer ? 1 : 0); 285 if ($pred->isTrue($this)) { 286 if ($seek === null) { 287 $seek = $size; 288 $blocks[] = $size; 289 } else if ($gap > 0) { 290 $blocks[] = $gap; //Don't remove the files between the gap 291 $blocks[] = $size; 292 $seek += $size; 293 } else { 294 $blocks[count($blocks)-1] += $size; //Also remove this file 295 $seek += $size; 296 } 297 $gap = 0; 298 } else { 299 if ($seek !== null) { 300 $seek += $size; 301 $gap += $size; 302 } 303 } 304 } 305 if ($seek === null) { 306 $seek = 0; 307 } else { 308 if ($gap == 0) { 309 array_pop($blocks); 310 } else { 311 $blocks[] = $gap; 312 } 313 } 314 315 $writer = new File_Archive_Writer_Ar(null, 316 $this->source->makeWriterRemoveBlocks($blocks, -$seek) 317 ); 318 $this->close(); 319 return $writer; 320 } 321 322 /** 323 * @see File_Archive_Reader::makeWriterRemoveBlocks() 324 */ 325 function makeWriterRemoveBlocks($blocks, $seek = 0) 326 { 327 if ($this->_currentStat === null) { 328 return PEAR::raiseError('No file selected'); 329 } 330 331 $blockPos = $this->_currentStat[7] - $this->_nbBytesLeft + $seek; 332 333 $this->rewind(); 334 $keep = false; 335 336 $data = $this->getData($blockPos); 337 foreach ($blocks as $length) { 338 if ($keep) { 339 $data .= $this->getData($length); 340 } else { 341 $this->skip($length); 342 } 343 $keep = !$keep; 344 } 345 if ($keep) { 346 $data .= $this->getData(); 347 } 348 349 $filename = $this->_currentFilename; 350 $stat = $this->_currentStat; 351 352 $writer = $this->makeWriterRemove(); 353 if (PEAR::isError($writer)) { 354 return $writer; 355 } 356 357 unset($stat[7]); 358 $writer->newFile($filename, $stat); 359 $writer->writeData($data); 360 return $writer; 361 } 362 363 /** 364 * @see File_Archive_Reader::makeAppendWriter 365 */ 366 function makeAppendWriter() 367 { 368 require_once "File/Archive/Writer/Ar.php"; 369 370 while (($error = $this->next()) === true) { } 371 if (PEAR::isError($error)) { 372 $this->close(); 373 return $error; 374 } 375 376 $innerWriter = $this->source->makeWriterRemoveBlocks(array()); 377 if (PEAR::isError($innerWriter)) { 378 return $innerWriter; 379 } 380 381 unset($this->source); 382 $this->close(); 383 384 return new File_Archive_Writer_Ar(null, $innerWriter); 385 } 386} 387?>