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?>