1<?php
2/**
3 * PHP_Archive Manager Class creator (allows debugging/manipulation of phar files)
4 *
5 * @package PHP_Archive
6 * @category PHP
7 */
8/**
9 * Needed for file manipulation
10 */
11require_once 'System.php';
12require_once 'PHP/Archive/Exception.php';
13
14
15/**
16 *
17 * @copyright Copyright ? Gregory Beaver
18 * @author Greg Beaver <cellog@php.net>
19 * @version $Id$
20 * @package PHP_Archive
21 * @category PHP
22 */
23class PHP_Archive_Manager
24{
25    const GZ = 0x00001000;
26    const BZ2 = 0x00002000;
27    const SIG = 0x00010000;
28    const SHA1 = 0x0002;
29    const MD5 = 0x0001;
30    private $_alias;
31    private $_archiveName;
32    private $_apiVersion;
33    private $_flags;
34    private $_knownAPIVersions = array('1.0.0', '1.1.0');
35    private $_manifest;
36    private $_fileStart;
37    private $_manifestSize;
38    private $_html;
39    private $_metadata;
40    private $_sigtype;
41    private $_signature = false;
42    /**
43     * Locate the .phar archive in the include_path and detect the file to open within
44     * the archive.
45     *
46     * Possible parameters are phar://filename_within_phar.ext or
47     * phar://pharname.phar/filename_within_phar.ext
48     *
49     * phar://filename_within_phar.ext will simply use the last .phar opened.
50     * @param string a file within the archive
51     * @return string the filename within the .phar to retrieve
52     */
53    public function __construct($phar)
54    {
55        $this->_archiveName = $phar;
56        $this->validate();
57    }
58
59    /**
60     * validate a phar prior to manipulating it
61     * @throws PHP_Archive_Exception
62     */
63    public function validate($strict = false)
64    {
65        $errors = array();
66        $warnings = array();
67        $fp = fopen($this->_archiveName, 'rb');
68        if (!$fp) {
69            throw new PHP_Archive_ExceptionExtended(PHP_Archive_ExceptionExtended::NOOPEN,
70                array('archive' => $this->_archiveName));
71        }
72        $header = fread($fp, strlen('<?php #PHP_ARCHIVE_HEADER-'));
73        if ($header == '<?php #PHP_ARCHIVE_HEADER-') {
74            $version = '';
75            while (!feof($fp) && (false !== $c = fgetc($fp))) {
76                if ((ord($c) < ord('0') || ord($c) > ord('9')) && $c != '.') {
77                    break;
78                }
79                $version .= $c;
80            }
81            if (version_compare($version, '0.8.0', '<')) {
82                throw new PHP_Archive_Exception($phar . ' was created with obsolete PHP_Archive',
83                    $errors);
84            }
85            $this->_version = $version;
86        }
87        // seek to __HALT_COMPILER_OFFSET__
88        $found = false;
89        while (!feof($fp) && false != ($next = fread($fp, 8192))) {
90            if (false != ($t = strpos($next, '__HALT_COMPILER();'))) {
91                fseek($fp, $t - strlen($next) + strlen('__HALT_COMPILER();'), SEEK_CUR);
92                $found = true;
93                break;
94            }
95        }
96        if (!$found) {
97            throw new PHP_Archive_ExceptionExtended(PHP_Archive_ExceptionExtended::NOTPHAR,
98                array('archive' => $this->_archiveName));
99        }
100        $manifest_length = fread($fp, 4);
101        $manifest_length = unpack('Vlen', $manifest_length);
102        $this->_manifestSize = $manifest_length = $manifest_length['len'];
103        if ($manifest_length > 1048576) {
104            if ($strict) {
105                throw new PHP_Archive_ExceptionExtended(
106                    PHP_Archive_ExceptionExtended::MANIFESTOVERFLOW, array(
107                    'archive' => $this->_archiveName));
108            }
109            $errors[] = new PHP_Archive_ExceptionExtended(
110                PHP_Archive_ExceptionExtended::MANIFESTOVERFLOW, array(
111                'archive' => $this->_archiveName));
112        }
113        $manifest = fread($fp, $manifest_length);
114        // retrieve the number of files in the manifest
115        $info = unpack('V', substr($manifest, 0, 4));
116        if ($info[1] * 24 > $manifest_length) {
117            $errors[] = new PHP_Archive_ExceptionExtended(
118                PHP_Archive_ExceptionExtended::MANIFESTENTRIESOVERFLOW,array(
119                'archive' => $this->_archiveName));
120            throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
121        }
122        $manifest = substr($manifest, 4);
123        if (strlen($manifest) < 4) {
124            $errors[] = new PHP_Archive_ExceptionExtended(
125                PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array(
126                'archive' => $this->_archiveName));
127            throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
128        }
129        // get API version and compressed flag
130        $apiver = substr($manifest, 0, 2);
131        $apiver = bin2hex($apiver);
132        $this->_apiVersion = hexdec($apiver[0]) . '.' . hexdec($apiver[1]) .
133            '.' . hexdec($apiver[2]);
134        if (!in_array($this->_apiVersion, $this->_knownAPIVersions)) {
135            $errors[] = new PHP_Archive_ExceptionExtended(
136                PHP_Archive_ExceptionExtended::UNKNOWNAPI, array(
137                'archive' => $this->_archiveName, 'ver' => $this->_apiVersion));
138            throw new PHP_Archive_Exception('phar "' . $this->_archiveName . '" cannot be analyzed', $errors);
139        }
140        $manifest = substr($manifest, 2);
141        if (strlen($manifest) < 4) {
142            $errors[] = new PHP_Archive_ExceptionExtended(
143                PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array(
144                'archive' => $this->_archiveName));
145            throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
146        }
147        // get flags
148        $flags = unpack('V', substr($manifest, 0, 4));
149        $this->_flags = $flags[1];
150        $manifest = substr($manifest, 4);
151        if (strlen($manifest) < 4) {
152            $errors[] = new PHP_Archive_ExceptionExtended(
153                PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array(
154                'archive' => $this->_archiveName));
155            throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
156        }
157        // get alias
158        $aliaslen = unpack('V', substr($manifest, 0, 4));
159        $aliaslen = $aliaslen[1];
160        $manifest = substr($manifest, 4);
161        if (strlen($manifest) < $aliaslen) {
162            $errors[] = new PHP_Archive_ExceptionExtended(
163                PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array(
164                'archive' => $this->_archiveName));
165            throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
166        }
167        $this->_alias = substr($manifest, 0, $aliaslen);
168        $manifest = substr($manifest, $aliaslen);
169        // phar metadata
170        if (strlen($manifest) < 4) {
171            $errors[] = new PHP_Archive_ExceptionExtended(
172                PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array(
173                'archive' => $this->_archiveName));
174            throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
175        }
176        $metadatalen = unpack('V', substr($manifest, 0, 4));
177        $metadatalen = $metadatalen[1];
178        $manifest = substr($manifest, 4);
179        if ($metadatalen) {
180            if (strlen($manifest) < $metadatalen) {
181                $errors[] = new PHP_Archive_ExceptionExtended(
182                    PHP_Archive_ExceptionExtended::MANIFESTENTRIESUNDERFLOW, array(
183                    'archive' => $this->_archiveName));
184                throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
185            }
186            $this->_metadata = unserialize(substr($manifest, 0, $metadatalen));
187            $manifest = substr($manifest, $metadatalen);
188        }
189        $ret = array();
190        $offset = 0;
191        for ($i = 0; $i < $info[1]; $i++) {
192            if (strlen($manifest) < 4) {
193                if (isset($savepath)) {
194                    $errors[] = new PHP_Archive_ExceptionExtended(
195                        PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array(
196                        'archive' => $this->_archiveName, 'last' => $savepath,
197                        'current' => '*unknown*', 'size' => $info[1], 'cur' => $i));
198                    throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
199                } else {
200                    $errors[] = new PHP_Archive_ExceptionExtended(
201                        PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array(
202                        'archive' => $this->_archiveName, 'last' => '*none*',
203                        'current' => '*unknown*', 'size' => $info[1], 'cur' => $i));
204                    throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
205                }
206            }
207            // length of the file name
208            $len = unpack('V', substr($manifest, 0, 4));
209            if (strlen($manifest) < $len[1] + 4) {
210                if (isset($savepath)) {
211                    $errors[] = new PHP_Archive_ExceptionExtended(
212                        PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array(
213                        'archive' => $this->_archiveName, 'last' => $savepath,
214                        'current' => '*unknown*', 'size' => $info[1], 'cur' => $i));
215                    throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
216                } else {
217                    $errors[] = new PHP_Archive_ExceptionExtended(
218                        PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array(
219                        'archive' => $this->_archiveName, 'last' => '*none*',
220                        'current' => '*unknown*', 'size' => $info[1], 'cur' => $i));
221                    throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
222                }
223            }
224            // file name
225            if (!isset($savepath)) {
226                $last = '*none*';
227            } else {
228                $last = $savepath;
229            }
230            $savepath = substr($manifest, 4, $len[1]);
231            $manifest = substr($manifest, $len[1] + 4);
232            if (strlen($manifest) < 24) {
233                if (isset($savepath)) {
234                    $errors[] = new PHP_Archive_ExceptionExtended(
235                        PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array(
236                        'archive' => $this->_archiveName, 'last' => $last,
237                        'current' => $savepath, 'size' => $info[1], 'cur' => $i));
238                    throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
239                } else {
240                    $errors[] = new PHP_Archive_ExceptionExtended(
241                        PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY, array(
242                        'archive' => $this->_archiveName, 'last' => $last,
243                        'current' => $savepath, 'size' => $info[1], 'cur' => $i));
244                    throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
245                }
246            }
247            // retrieve manifest data:
248            // 0 = uncompressed file size
249            // 1 = save timestamp
250            // 2 = compressed file size
251            // 3 = crc32
252            // 4 = flags
253            // 5 = metadata length
254            $ret[$savepath] = array_values(unpack('Va/Vb/Vc/Vd/Ve/Vf', substr($manifest, 0, 24)));
255            $ret[$savepath][3] = sprintf('%u', $ret[$savepath][3]
256                & 0xffffffff);
257            $manifest = substr($manifest, 24);
258            if ($ret[$savepath][5]) {
259                if (strlen($manifest) < $ret[$savepath][5]) {
260                    $errors[] = new PHP_Archive_ExceptionExtended(
261                        PHP_Archive_ExceptionExtended::MANIFESTENTRIESTRUNCATEDENTRY,
262                        array('archive' => $this->_archiveName, 'last' => $last,
263                        'current' => $savepath, 'size' => $info[1], 'cur' => $i));
264                    throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName .
265                        '"', $errors);
266                }
267                $ret[$savepath][6] = unserialize(fread($fp, $ret[$savepath][5]));
268            }
269            $ret[$savepath][7] = $offset;
270            $offset += $ret[$savepath][2];
271        }
272        $this->_manifest =  $ret;
273        $this->_fileStart = ftell($fp);
274        foreach ($this->_manifest as $path => $info) {
275            $currentFilename = $path;
276            $internalFileLength = $info[2];
277            // seek to offset of file header within the .phar
278            if (fseek($fp, $this->_fileStart + $info[7])) {
279                $errors[] = new PHP_Archive_ExceptionExtended(
280                    PHP_Archive_ExceptionExtended::FILELOCATIONINVALID,
281                    array('archive' => $this->_archiveName, 'file' => $path, 'loc' => $this->_fileStart + $info[7],
282                    'size' => filesize($this->_archiveName)));
283                continue;
284            }
285            $temp = array('crc32' => $info[3], 'isize' => $info[0]);
286            $data = '';
287            $count = $internalFileLength;
288            while ($count) {
289                if ($count < 8192) {
290                    $data .= @fread($fp, $count);
291                    $count = 0;
292                } else {
293                    $count -= 8192;
294                    $data .= @fread($fp, 8192);
295                }
296            }
297            if ($info[4] & self::GZ) {
298                $data = @gzinflate($data);
299                if ($data === false) {
300                    $errors[] = new PHP_Archive_ExceptionExtended(
301                        PHP_Archive_ExceptionExtended::FILECORRUPTEDGZ,
302                        array('archive' => $this->_archiveName, 'file' => $path, 'loc' => $this->_fileStart + $info[2]));
303                }
304            }
305            if ($info[4] & self::BZ2) {
306                $data = @bzdecompress($data, true);
307                if ($data === false) {
308                    $errors[] = new PHP_Archive_ExceptionExtended(
309                        PHP_Archive_ExceptionExtended::FILECORRUPTEDBZ2,
310                        array('archive' => $this->_archiveName, 'file' => $path, 'loc' => $this->_fileStart + $info[2]));
311                }
312            }
313            if ($temp['isize'] != strlen($data)) {
314                $errors[] = new PHP_Archive_ExceptionExtended(
315                    PHP_Archive_ExceptionExtended::FILECORRUPTEDSIZE,
316                    array('archive' => $this->_archiveName, 'file' => $path, 'expected' => $temp['isize'],
317                        'actual' => strlen($data)));
318            }
319            if ($temp['crc32'] != sprintf("%u", crc32($data) & 0xffffffff)) {
320                $errors[] = new PHP_Archive_ExceptionExtended(
321                    PHP_Archive_ExceptionExtended::FILECORRUPTEDCRC,
322                    array('archive' => $this->_archiveName, 'file' => $path, 'expected' => $temp['crc32'],
323                        'actual' => crc32($data)));
324            }
325        }
326        if ($this->_flags & self::SIG) {
327            do {
328                $end = ftell($fp);
329                $data = fread($fp, 28);
330                if (substr($data, strlen($data) - 4) != 'GBMB') {
331                    $errors[] = new PHP_Archive_ExceptionExtended(
332                        PHP_Archive_ExceptionExtended::NOSIGNATUREMAGIC,
333                        array('archive' => $this->_archiveName));
334                    break;
335                }
336                $type = unpack('V', substr($data, strlen($data) - 8, 4));
337                $all = file_get_contents($this->_archiveName);
338                switch ($type[1]) {
339                    case self::MD5 :
340                        $hash = substr($all, strlen($all) - 16 - 8, 16);
341                        $all = substr($all, 0, strlen($all) - 16 - 8);
342                        if (md5(substr($all, 0, $end), true) != $hash) {
343                            $errors[] = new PHP_Archive_ExceptionExtended(
344                                PHP_Archive_ExceptionExtended::BADSIGNATURE,
345                                array('archive' => $this->_archiveName));
346                        } else {
347                            $this->_sigtype = 'MD5';
348                            $this->_signature = md5($all);
349                        }
350                        break;
351                    case self::SHA1 :
352                        $hash = substr($all, strlen($all) - 20 - 8, 20);
353                        if (sha1(substr($all, 0, $end), true) != $hash) {
354                            $errors[] = new PHP_Archive_ExceptionExtended(
355                                PHP_Archive_ExceptionExtended::BADSIGNATURE,
356                                array('archive' => $this->_archiveName));
357                        } else {
358                            $this->_sigtype = 'SHA1';
359                            $this->_signature = sha1($all);
360                        }
361                        break;
362                    default :
363                        $errors[] = new PHP_Archive_ExceptionExtended(
364                            PHP_Archive_ExceptionExtended::UNKNOWNSIGTYPE,
365                            array('archive' => $this->_archiveName,
366                                'type' => bin2hex(substr($data, strlen($data) - 8, 4))));
367                        break;
368                }
369            } while (false);
370        }
371        @fclose($fp);
372        if (count($errors)) {
373            throw new PHP_Archive_Exception('invalid phar "' . $this->_archiveName . '"', $errors);
374        }
375    }
376
377    /**
378     * Display information on a phar
379     *
380     * @param bool
381     */
382    public function dump($return_array = false)
383    {
384        if (!$return_array) {
385            echo $this;
386            return;
387        }
388        $filesize = filesize($this->_archiveName);
389        $ret = array(
390            'Phar name' => $this->_archiveName,
391            'Size' => $filesize,
392            'API version' => $this->_apiVersion,
393            'Manifest size (bytes)' => $this->_manifestSize,
394            'Manifest entries' => count($this->_manifest),
395            'Alias' => $this->_alias,
396            'Phar Metadata' => var_export($this->_metadata, true),
397            'Global compressed flag' => bin2hex($this->_flags),
398        );
399        if ($this->_signature) {
400            $ret['Signature Type'] = $this->_sigtype;
401            $ret['Signature'] = $this->_signature;
402        }
403        // 0 = uncompressed file size
404        // 1 = save timestamp
405        // 2 = compressed file size
406        // 3 = crc32
407        // 4 = flags
408        // 5 = meta-data length
409        // 6 = meta-data
410        $offset = 0;
411        foreach ($this->_manifest as $file => $info) {
412            $ret['File phar://' . $this->_alias . '/' . $file . ' size'] = $info[0];
413            $ret['File phar://' . $this->_alias . '/' . $file . ' save date'] =
414                date('Y-m-d H:i', $info[1]);
415            $ret['File phar://' . $this->_alias . '/' . $file . ' crc'] = $info[3];
416            $ret['File phar://' . $this->_alias . '/' . $file . ' size in archive'] = $info[2];
417            $ret['File phar://' . $this->_alias . '/' . $file . ' offset in archive'] = $offset;
418            $ret['File phar://' . $this->_alias . '/' . $file . ' meta-data length'] = $info[5];
419            $ret['File phar://' . $this->_alias . '/' . $file . ' meta-data'] =
420                var_export($info[6], true);
421            $ret['File phar://' . $this->_alias . '/' . $file . ' GZ compressed'] =
422                $info[4] & self::GZ ? 'yes' : 'no';
423            $ret['File phar://' . $this->_alias . '/' . $file . ' BZ2 compressed'] =
424                $info[4] & self::BZ2 ? 'yes' : 'no';
425            $offset += $info[2];
426        }
427        return $ret;
428    }
429
430    public function __toString()
431    {
432        $ret = $this->dump(true);
433        if ($this->_html) {
434            array_walk($ret, function(&$a, $b) { $a = "<strong>$b:</strong> $a"; });
435            $ret = implode("<br />\n", $ret);
436        } else {
437            array_walk($ret, function(&$a, $b) { $a = "$b: $a"; });
438            $ret = implode("\n", $ret);
439        }
440        return $ret;
441    }
442
443    /**
444     * Extract the .phar to a particular location
445     *
446     * @param string $toHere
447     */
448    public function unPhar($toHere)
449    {
450
451    }
452
453    /**
454     * Re-make the phar from a previously after having done work on an unPharred phar
455     *
456     * @param string $fromHere
457     */
458    public function rePhar($fromHere)
459    {
460
461    }
462
463    /**
464     * For display of data in a browser
465     *
466     * @return PHP_Archive_Manager
467     */
468    public function inHtml()
469    {
470        $this->_html = true;
471        $a = clone $this;
472        $this->_html = false;
473        return $a;
474    }
475}
476?>
477