1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4/**
5 * Read a tar archive
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: Tar.php,v 1.29 2005/07/11 11:53:53 vincentlascaux Exp $
29 * @link       http://pear.php.net/package/File_Archive
30 */
31
32require_once "File/Archive/Reader/Archive.php";
33
34/**
35 * Read a tar archive
36 */
37class File_Archive_Reader_Tar extends File_Archive_Reader_Archive
38{
39    /**
40     * @var String Name of the file being read
41     * @access private
42     */
43    var $currentFilename = null;
44    /**
45     * @var Array Stats of the file being read
46     *            In TAR reader, indexes 2, 4, 5, 7, 9 are set
47     * @access private
48     */
49    var $currentStat = null;
50    /**
51     * @var int Number of bytes that still have to be read before the end of
52     *          file
53     * @access private
54     */
55    var $leftLength = 0;
56    /**
57     * @var int Size of the footer
58     *          A TAR file is made of chunks of 512 bytes. If 512 does not
59     *          divide the file size a footer is added
60     * @access private
61     */
62    var $footerLength = 0;
63    /**
64     * @var int nb bytes to seek back in order to reach the end of the archive
65     *          or null if the end of the archive has not been reached
66     */
67    var $seekToEnd = null;
68
69    /**
70     * @see File_Archive_Reader::skip()
71     */
72    function skip($length = -1)
73    {
74        if ($length == -1) {
75            $length = $this->leftLength;
76        } else {
77            $length = min($this->leftLength, $length);
78        }
79        $skipped = $this->source->skip($length);
80        if (!PEAR::isError($skipped)) {
81            $this->leftLength -= $skipped;
82        }
83        return $skipped;
84    }
85
86    /**
87     * @see File_Archive_Reader::rewind()
88     */
89    function rewind($length = -1)
90    {
91        if ($length == -1) {
92            $length = $this->currentStat[7] - $this->leftLength;
93        } else {
94            $length = min($length, $this->currentStat[7] - $this->leftLength);
95        }
96        $rewinded = $this->source->rewind($length);
97        if (!PEAR::isError($rewinded)) {
98            $this->leftLength += $rewinded;
99        }
100        return $rewinded;
101    }
102
103    /**
104     * @see File_Archive_Reader::tell()
105     */
106    function tell()
107    {
108        return $this->currentStat[7] - $this->leftLength;
109    }
110
111    /**
112     * @see File_Archive_Reader::close()
113     */
114    function close()
115    {
116        $this->leftLength = 0;
117        $this->currentFilename = null;
118        $this->currentStat = null;
119        $this->seekToEnd = null;
120        return parent::close();
121    }
122
123    /**
124     * @see File_Archive_Reader::getFilename()
125     */
126    function getFilename() { return $this->currentFilename; }
127    /**
128     * @see File_Archive_Reader::getStat()
129     */
130    function getStat() { return $this->currentStat; }
131
132    /**
133     * @see File_Archive_Reader::next()
134     */
135    function next()
136    {
137        $error = parent::next();
138        if ($error !== true) {
139            return $error;
140        }
141        if ($this->seekToEnd !== null) {
142            return false;
143        }
144
145        do
146        {
147            $error = $this->source->skip($this->leftLength + $this->footerLength);
148            if (PEAR::isError($error)) {
149                return $error;
150            }
151            $rawHeader = $this->source->getData(512);
152            if (PEAR::isError($rawHeader)) {
153                return $rawHeader;
154            }
155            if (strlen($rawHeader)<512 || $rawHeader == pack("a512", "")) {
156                $this->seekToEnd = strlen($rawHeader);
157                $this->currentFilename = null;
158                return false;
159            }
160
161            $header = unpack(
162                "a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/".
163                "a8checksum/a1type/a100linkname/a6magic/a2version/".
164                "a32uname/a32gname/a8devmajor/a8devminor/a155prefix",
165                $rawHeader);
166            $this->currentStat = array(
167                2 => octdec($header['mode']),
168                4 => octdec($header['uid']),
169                5 => octdec($header['gid']),
170                7 => octdec($header['size']),
171                9 => octdec($header['mtime'])
172                );
173            $this->currentStat['mode']  = $this->currentStat[2];
174            $this->currentStat['uid']   = $this->currentStat[4];
175            $this->currentStat['gid']   = $this->currentStat[5];
176            $this->currentStat['size']  = $this->currentStat[7];
177            $this->currentStat['mtime'] = $this->currentStat[9];
178
179            if ($header['magic'] == 'ustar') {
180                $this->currentFilename = $this->getStandardURL(
181                                $header['prefix'] . $header['filename']
182                            );
183            } else {
184                $this->currentFilename = $this->getStandardURL(
185                                $header['filename']
186                            );
187            }
188
189            $this->leftLength = $this->currentStat[7];
190            if ($this->leftLength % 512 == 0) {
191                $this->footerLength = 0;
192            } else {
193                $this->footerLength = 512 - $this->leftLength%512;
194            }
195
196            $checksum = 8*ord(" ");
197            for ($i = 0; $i < 148; $i++) {
198                $checksum += ord($rawHeader{$i});
199            }
200            for ($i = 156; $i < 512; $i++) {
201                $checksum += ord($rawHeader{$i});
202            }
203
204            if (octdec($header['checksum']) != $checksum) {
205                die('Checksum error on entry '.$this->currentFilename);
206            }
207        } while ($header['type'] != 0);
208
209        return true;
210    }
211
212    /**
213     * @see File_Archive_Reader::getData()
214     */
215    function getData($length = -1)
216    {
217        if ($length == -1) {
218            $actualLength = $this->leftLength;
219        } else {
220            $actualLength = min($this->leftLength, $length);
221        }
222
223        if ($this->leftLength == 0) {
224            return null;
225        } else {
226            $data = $this->source->getData($actualLength);
227            if (strlen($data) != $actualLength) {
228                return PEAR::raiseError('Unexpected end of tar archive');
229            }
230            $this->leftLength -= $actualLength;
231            return $data;
232        }
233    }
234
235    /**
236     * @see File_Archive_Reader::makeWriterRemoveFiles()
237     */
238    function makeWriterRemoveFiles($pred)
239    {
240        require_once "File/Archive/Writer/Tar.php";
241
242        $blocks = array();
243        $seek = null;
244        $gap = 0;
245        if ($this->currentFilename !== null && $pred->isTrue($this)) {
246            $seek = 512 + $this->currentStat[7] + $this->footerLength;
247            $blocks[] = $seek; //Remove this file
248        }
249
250        while (($error = $this->next()) === true) {
251            $size = 512 + $this->currentStat[7] + $this->footerLength;
252            if ($pred->isTrue($this)) {
253                if ($seek === null) {
254                    $seek = $size;
255                    $blocks[] = $size;
256                } else if ($gap > 0) {
257                    $blocks[] = $gap; //Don't remove the files between the gap
258                    $blocks[] = $size;
259                    $seek += $size;
260                } else {
261                    $blocks[count($blocks)-1] += $size;   //Also remove this file
262                    $seek += $size;
263                }
264                $gap = 0;
265            } else {
266                if ($seek !== null) {
267                    $seek += $size;
268                    $gap += $size;
269                }
270            }
271        }
272        if ($seek === null) {
273            $seek = $this->seekToEnd;
274        } else {
275            $seek += $this->seekToEnd;
276            if ($gap == 0) {
277                array_pop($blocks);
278            } else {
279                $blocks[] = $gap;
280            }
281        }
282
283        $writer = new File_Archive_Writer_Tar(null,
284            $this->source->makeWriterRemoveBlocks($blocks, -$seek)
285        );
286        $this->close();
287        return $writer;
288    }
289
290    /**
291     * @see File_Archive_Reader::makeWriterRemoveBlocks()
292     */
293    function makeWriterRemoveBlocks($blocks, $seek = 0)
294    {
295        if ($this->seekToEnd !== null || $this->currentStat === null) {
296            return PEAR::raiseError('No file selected');
297        }
298
299        $blockPos = $this->currentStat[7] - $this->leftLength + $seek;
300
301        $this->rewind();
302        $keep = false;
303
304        $data = $this->getData($blockPos);
305        foreach ($blocks as $length) {
306            if ($keep) {
307                $data .= $this->getData($length);
308            } else {
309                $this->skip($length);
310            }
311            $keep = !$keep;
312        }
313        if ($keep) {
314            $data .= $this->getData();
315        }
316
317        $filename = $this->currentFilename;
318        $stat = $this->currentStat;
319
320        $writer = $this->makeWriterRemove();
321        if (PEAR::isError($writer)) {
322            return $writer;
323        }
324
325        unset($stat[7]);
326        $stat[9] = $stat['mtime'] = time();
327        $writer->newFile($filename, $stat);
328        $writer->writeData($data);
329        return $writer;
330    }
331
332    /**
333     * @see File_Archive_Reader::makeAppendWriter
334     */
335    function makeAppendWriter()
336    {
337        require_once "File/Archive/Writer/Tar.php";
338
339        while (($error = $this->next()) === true) { }
340        if (PEAR::isError($error)) {
341            $this->close();
342            return $error;
343        }
344
345        $innerWriter = $this->source->makeWriterRemoveBlocks(array(), -$this->seekToEnd);
346        if (PEAR::isError($innerWriter)) {
347            return $innerWriter;
348        }
349
350        $this->close();
351        return new File_Archive_Writer_Tar(null, $innerWriter);
352    }
353}
354
355?>