xref: /plugin/upgrade/myvendor/splitbrain/php-archive/src/Tar.php (revision ab8e5256dbaece2751c4414d6cf7b761cb71998a)
1*ab8e5256SAndreas Gohr<?php
2*ab8e5256SAndreas Gohr
3*ab8e5256SAndreas Gohrnamespace splitbrain\PHPArchive;
4*ab8e5256SAndreas Gohr
5*ab8e5256SAndreas Gohr/**
6*ab8e5256SAndreas Gohr * Class Tar
7*ab8e5256SAndreas Gohr *
8*ab8e5256SAndreas Gohr * Creates or extracts Tar archives. Supports gz and bzip compression
9*ab8e5256SAndreas Gohr *
10*ab8e5256SAndreas Gohr * Long pathnames (>100 chars) are supported in POSIX ustar and GNU longlink formats.
11*ab8e5256SAndreas Gohr *
12*ab8e5256SAndreas Gohr * @author  Andreas Gohr <andi@splitbrain.org>
13*ab8e5256SAndreas Gohr * @package splitbrain\PHPArchive
14*ab8e5256SAndreas Gohr * @license MIT
15*ab8e5256SAndreas Gohr */
16*ab8e5256SAndreas Gohrclass Tar extends Archive
17*ab8e5256SAndreas Gohr{
18*ab8e5256SAndreas Gohr    const READ_CHUNK_SIZE = 1048576; // 1MB
19*ab8e5256SAndreas Gohr
20*ab8e5256SAndreas Gohr    protected $file = '';
21*ab8e5256SAndreas Gohr    protected $comptype = Archive::COMPRESS_AUTO;
22*ab8e5256SAndreas Gohr    protected $complevel = 9;
23*ab8e5256SAndreas Gohr    protected $fh;
24*ab8e5256SAndreas Gohr    protected $memory = '';
25*ab8e5256SAndreas Gohr    protected $closed = true;
26*ab8e5256SAndreas Gohr    protected $writeaccess = false;
27*ab8e5256SAndreas Gohr    protected $position = 0;
28*ab8e5256SAndreas Gohr    protected $contentUntil = 0;
29*ab8e5256SAndreas Gohr    protected $skipUntil = 0;
30*ab8e5256SAndreas Gohr
31*ab8e5256SAndreas Gohr    /**
32*ab8e5256SAndreas Gohr     * Sets the compression to use
33*ab8e5256SAndreas Gohr     *
34*ab8e5256SAndreas Gohr     * @param int $level Compression level (0 to 9)
35*ab8e5256SAndreas Gohr     * @param int $type Type of compression to use (use COMPRESS_* constants)
36*ab8e5256SAndreas Gohr     * @throws ArchiveIllegalCompressionException
37*ab8e5256SAndreas Gohr     */
38*ab8e5256SAndreas Gohr    public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
39*ab8e5256SAndreas Gohr    {
40*ab8e5256SAndreas Gohr        $this->compressioncheck($type);
41*ab8e5256SAndreas Gohr        if ($level < -1 || $level > 9) {
42*ab8e5256SAndreas Gohr            throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9');
43*ab8e5256SAndreas Gohr        }
44*ab8e5256SAndreas Gohr        $this->comptype  = $type;
45*ab8e5256SAndreas Gohr        $this->complevel = $level;
46*ab8e5256SAndreas Gohr        if($level == 0) $this->comptype = Archive::COMPRESS_NONE;
47*ab8e5256SAndreas Gohr        if($type == Archive::COMPRESS_NONE) $this->complevel = 0;
48*ab8e5256SAndreas Gohr    }
49*ab8e5256SAndreas Gohr
50*ab8e5256SAndreas Gohr    /**
51*ab8e5256SAndreas Gohr     * Open an existing TAR file for reading
52*ab8e5256SAndreas Gohr     *
53*ab8e5256SAndreas Gohr     * @param string $file
54*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
55*ab8e5256SAndreas Gohr     * @throws ArchiveIllegalCompressionException
56*ab8e5256SAndreas Gohr     */
57*ab8e5256SAndreas Gohr    public function open($file)
58*ab8e5256SAndreas Gohr    {
59*ab8e5256SAndreas Gohr        $this->file = $file;
60*ab8e5256SAndreas Gohr
61*ab8e5256SAndreas Gohr        // update compression to mach file
62*ab8e5256SAndreas Gohr        if ($this->comptype == Tar::COMPRESS_AUTO) {
63*ab8e5256SAndreas Gohr            $this->setCompression($this->complevel, $this->filetype($file));
64*ab8e5256SAndreas Gohr        }
65*ab8e5256SAndreas Gohr
66*ab8e5256SAndreas Gohr        // open file handles
67*ab8e5256SAndreas Gohr        if ($this->comptype === Archive::COMPRESS_GZIP) {
68*ab8e5256SAndreas Gohr            $this->fh = @gzopen($this->file, 'rb');
69*ab8e5256SAndreas Gohr        } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
70*ab8e5256SAndreas Gohr            $this->fh = @bzopen($this->file, 'r');
71*ab8e5256SAndreas Gohr        } else {
72*ab8e5256SAndreas Gohr            $this->fh = @fopen($this->file, 'rb');
73*ab8e5256SAndreas Gohr        }
74*ab8e5256SAndreas Gohr
75*ab8e5256SAndreas Gohr        if (!$this->fh) {
76*ab8e5256SAndreas Gohr            throw new ArchiveIOException('Could not open file for reading: '.$this->file);
77*ab8e5256SAndreas Gohr        }
78*ab8e5256SAndreas Gohr        $this->closed = false;
79*ab8e5256SAndreas Gohr        $this->position = 0;
80*ab8e5256SAndreas Gohr    }
81*ab8e5256SAndreas Gohr
82*ab8e5256SAndreas Gohr    /**
83*ab8e5256SAndreas Gohr     * Read the contents of a TAR archive
84*ab8e5256SAndreas Gohr     *
85*ab8e5256SAndreas Gohr     * This function lists the files stored in the archive
86*ab8e5256SAndreas Gohr     *
87*ab8e5256SAndreas Gohr     * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
88*ab8e5256SAndreas Gohr     * Reopen the file with open() again if you want to do additional operations
89*ab8e5256SAndreas Gohr     *
90*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
91*ab8e5256SAndreas Gohr     * @throws ArchiveCorruptedException
92*ab8e5256SAndreas Gohr     * @returns FileInfo[]
93*ab8e5256SAndreas Gohr     */
94*ab8e5256SAndreas Gohr    public function contents()
95*ab8e5256SAndreas Gohr    {
96*ab8e5256SAndreas Gohr        $result = array();
97*ab8e5256SAndreas Gohr
98*ab8e5256SAndreas Gohr        foreach ($this->yieldContents() as $fileinfo) {
99*ab8e5256SAndreas Gohr            $result[] = $fileinfo;
100*ab8e5256SAndreas Gohr        }
101*ab8e5256SAndreas Gohr
102*ab8e5256SAndreas Gohr        return $result;
103*ab8e5256SAndreas Gohr    }
104*ab8e5256SAndreas Gohr
105*ab8e5256SAndreas Gohr    /**
106*ab8e5256SAndreas Gohr     * Read the contents of a TAR archive and return each entry using yield
107*ab8e5256SAndreas Gohr     * for memory efficiency.
108*ab8e5256SAndreas Gohr     *
109*ab8e5256SAndreas Gohr     * @see contents()
110*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
111*ab8e5256SAndreas Gohr     * @throws ArchiveCorruptedException
112*ab8e5256SAndreas Gohr     * @returns FileInfo[]
113*ab8e5256SAndreas Gohr     */
114*ab8e5256SAndreas Gohr    public function yieldContents()
115*ab8e5256SAndreas Gohr    {
116*ab8e5256SAndreas Gohr        if ($this->closed || !$this->file) {
117*ab8e5256SAndreas Gohr            throw new ArchiveIOException('Can not read from a closed archive');
118*ab8e5256SAndreas Gohr        }
119*ab8e5256SAndreas Gohr
120*ab8e5256SAndreas Gohr        while ($read = $this->readbytes(512)) {
121*ab8e5256SAndreas Gohr            $header = $this->parseHeader($read);
122*ab8e5256SAndreas Gohr            if (!is_array($header)) {
123*ab8e5256SAndreas Gohr                continue;
124*ab8e5256SAndreas Gohr            }
125*ab8e5256SAndreas Gohr
126*ab8e5256SAndreas Gohr            $this->contentUntil = $this->position + $header['size'];
127*ab8e5256SAndreas Gohr            $this->skipUntil = $this->position + ceil($header['size'] / 512) * 512;
128*ab8e5256SAndreas Gohr
129*ab8e5256SAndreas Gohr            yield $this->header2fileinfo($header);
130*ab8e5256SAndreas Gohr
131*ab8e5256SAndreas Gohr            $skip = $this->skipUntil - $this->position;
132*ab8e5256SAndreas Gohr            if ($skip > 0) {
133*ab8e5256SAndreas Gohr                $this->skipbytes($skip);
134*ab8e5256SAndreas Gohr            }
135*ab8e5256SAndreas Gohr        }
136*ab8e5256SAndreas Gohr
137*ab8e5256SAndreas Gohr        $this->close();
138*ab8e5256SAndreas Gohr    }
139*ab8e5256SAndreas Gohr
140*ab8e5256SAndreas Gohr    /**
141*ab8e5256SAndreas Gohr     * Reads content of a current archive entry.
142*ab8e5256SAndreas Gohr     *
143*ab8e5256SAndreas Gohr     * Works only when iterating trough the archive using the generator returned
144*ab8e5256SAndreas Gohr     * by the yieldContents().
145*ab8e5256SAndreas Gohr     *
146*ab8e5256SAndreas Gohr     * @param int $length maximum number of bytes to read
147*ab8e5256SAndreas Gohr     *
148*ab8e5256SAndreas Gohr     * @return string
149*ab8e5256SAndreas Gohr     */
150*ab8e5256SAndreas Gohr    public function readCurrentEntry($length = PHP_INT_MAX)
151*ab8e5256SAndreas Gohr    {
152*ab8e5256SAndreas Gohr        $length = (int) min($length, $this->contentUntil - $this->position);
153*ab8e5256SAndreas Gohr        if ($length === 0) {
154*ab8e5256SAndreas Gohr            return '';
155*ab8e5256SAndreas Gohr        }
156*ab8e5256SAndreas Gohr        return $this->readbytes($length);
157*ab8e5256SAndreas Gohr    }
158*ab8e5256SAndreas Gohr
159*ab8e5256SAndreas Gohr    /**
160*ab8e5256SAndreas Gohr     * Extract an existing TAR archive
161*ab8e5256SAndreas Gohr     *
162*ab8e5256SAndreas Gohr     * The $strip parameter allows you to strip a certain number of path components from the filenames
163*ab8e5256SAndreas Gohr     * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
164*ab8e5256SAndreas Gohr     * an integer is passed as $strip.
165*ab8e5256SAndreas Gohr     * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
166*ab8e5256SAndreas Gohr     * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
167*ab8e5256SAndreas Gohr     *
168*ab8e5256SAndreas Gohr     * By default this will extract all files found in the archive. You can restrict the output using the $include
169*ab8e5256SAndreas Gohr     * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
170*ab8e5256SAndreas Gohr     * $include is set only files that match this expression will be extracted. Files that match the $exclude
171*ab8e5256SAndreas Gohr     * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
172*ab8e5256SAndreas Gohr     * stripped filenames as described above.
173*ab8e5256SAndreas Gohr     *
174*ab8e5256SAndreas Gohr     * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
175*ab8e5256SAndreas Gohr     * Reopen the file with open() again if you want to do additional operations
176*ab8e5256SAndreas Gohr     *
177*ab8e5256SAndreas Gohr     * @param string $outdir the target directory for extracting
178*ab8e5256SAndreas Gohr     * @param int|string $strip either the number of path components or a fixed prefix to strip
179*ab8e5256SAndreas Gohr     * @param string $exclude a regular expression of files to exclude
180*ab8e5256SAndreas Gohr     * @param string $include a regular expression of files to include
181*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
182*ab8e5256SAndreas Gohr     * @throws ArchiveCorruptedException
183*ab8e5256SAndreas Gohr     * @return FileInfo[]
184*ab8e5256SAndreas Gohr     */
185*ab8e5256SAndreas Gohr    public function extract($outdir, $strip = '', $exclude = '', $include = '')
186*ab8e5256SAndreas Gohr    {
187*ab8e5256SAndreas Gohr        if ($this->closed || !$this->file) {
188*ab8e5256SAndreas Gohr            throw new ArchiveIOException('Can not read from a closed archive');
189*ab8e5256SAndreas Gohr        }
190*ab8e5256SAndreas Gohr
191*ab8e5256SAndreas Gohr        $outdir = rtrim($outdir, '/');
192*ab8e5256SAndreas Gohr        @mkdir($outdir, 0777, true);
193*ab8e5256SAndreas Gohr        if (!is_dir($outdir)) {
194*ab8e5256SAndreas Gohr            throw new ArchiveIOException("Could not create directory '$outdir'");
195*ab8e5256SAndreas Gohr        }
196*ab8e5256SAndreas Gohr
197*ab8e5256SAndreas Gohr        $extracted = array();
198*ab8e5256SAndreas Gohr        while ($dat = $this->readbytes(512)) {
199*ab8e5256SAndreas Gohr            // read the file header
200*ab8e5256SAndreas Gohr            $header = $this->parseHeader($dat);
201*ab8e5256SAndreas Gohr            if (!is_array($header)) {
202*ab8e5256SAndreas Gohr                continue;
203*ab8e5256SAndreas Gohr            }
204*ab8e5256SAndreas Gohr            $fileinfo = $this->header2fileinfo($header);
205*ab8e5256SAndreas Gohr
206*ab8e5256SAndreas Gohr            // apply strip rules
207*ab8e5256SAndreas Gohr            $fileinfo->strip($strip);
208*ab8e5256SAndreas Gohr
209*ab8e5256SAndreas Gohr            // skip unwanted files
210*ab8e5256SAndreas Gohr            if (!strlen($fileinfo->getPath()) || !$fileinfo->matchExpression($include, $exclude)) {
211*ab8e5256SAndreas Gohr                $this->skipbytes(ceil($header['size'] / 512) * 512);
212*ab8e5256SAndreas Gohr                continue;
213*ab8e5256SAndreas Gohr            }
214*ab8e5256SAndreas Gohr
215*ab8e5256SAndreas Gohr            // create output directory
216*ab8e5256SAndreas Gohr            $output    = $outdir.'/'.$fileinfo->getPath();
217*ab8e5256SAndreas Gohr            $directory = ($fileinfo->getIsdir()) ? $output : dirname($output);
218*ab8e5256SAndreas Gohr            if (!file_exists($directory)) {
219*ab8e5256SAndreas Gohr                mkdir($directory, 0777, true);
220*ab8e5256SAndreas Gohr            }
221*ab8e5256SAndreas Gohr
222*ab8e5256SAndreas Gohr            // extract data
223*ab8e5256SAndreas Gohr            if (!$fileinfo->getIsdir()) {
224*ab8e5256SAndreas Gohr                $fp = @fopen($output, "wb");
225*ab8e5256SAndreas Gohr                if (!$fp) {
226*ab8e5256SAndreas Gohr                    throw new ArchiveIOException('Could not open file for writing: '.$output);
227*ab8e5256SAndreas Gohr                }
228*ab8e5256SAndreas Gohr
229*ab8e5256SAndreas Gohr                $size = floor($header['size'] / 512);
230*ab8e5256SAndreas Gohr                for ($i = 0; $i < $size; $i++) {
231*ab8e5256SAndreas Gohr                    fwrite($fp, $this->readbytes(512), 512);
232*ab8e5256SAndreas Gohr                }
233*ab8e5256SAndreas Gohr                if (($header['size'] % 512) != 0) {
234*ab8e5256SAndreas Gohr                    fwrite($fp, $this->readbytes(512), $header['size'] % 512);
235*ab8e5256SAndreas Gohr                }
236*ab8e5256SAndreas Gohr
237*ab8e5256SAndreas Gohr                fclose($fp);
238*ab8e5256SAndreas Gohr                @touch($output, $fileinfo->getMtime());
239*ab8e5256SAndreas Gohr                @chmod($output, $fileinfo->getMode());
240*ab8e5256SAndreas Gohr            } else {
241*ab8e5256SAndreas Gohr                $this->skipbytes(ceil($header['size'] / 512) * 512); // the size is usually 0 for directories
242*ab8e5256SAndreas Gohr            }
243*ab8e5256SAndreas Gohr
244*ab8e5256SAndreas Gohr            if(is_callable($this->callback)) {
245*ab8e5256SAndreas Gohr                call_user_func($this->callback, $fileinfo);
246*ab8e5256SAndreas Gohr            }
247*ab8e5256SAndreas Gohr            $extracted[] = $fileinfo;
248*ab8e5256SAndreas Gohr        }
249*ab8e5256SAndreas Gohr
250*ab8e5256SAndreas Gohr        $this->close();
251*ab8e5256SAndreas Gohr        return $extracted;
252*ab8e5256SAndreas Gohr    }
253*ab8e5256SAndreas Gohr
254*ab8e5256SAndreas Gohr    /**
255*ab8e5256SAndreas Gohr     * Create a new TAR file
256*ab8e5256SAndreas Gohr     *
257*ab8e5256SAndreas Gohr     * If $file is empty, the tar file will be created in memory
258*ab8e5256SAndreas Gohr     *
259*ab8e5256SAndreas Gohr     * @param string $file
260*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
261*ab8e5256SAndreas Gohr     * @throws ArchiveIllegalCompressionException
262*ab8e5256SAndreas Gohr     */
263*ab8e5256SAndreas Gohr    public function create($file = '')
264*ab8e5256SAndreas Gohr    {
265*ab8e5256SAndreas Gohr        $this->file   = $file;
266*ab8e5256SAndreas Gohr        $this->memory = '';
267*ab8e5256SAndreas Gohr        $this->fh     = 0;
268*ab8e5256SAndreas Gohr
269*ab8e5256SAndreas Gohr        if ($this->file) {
270*ab8e5256SAndreas Gohr            // determine compression
271*ab8e5256SAndreas Gohr            if ($this->comptype == Archive::COMPRESS_AUTO) {
272*ab8e5256SAndreas Gohr                $this->setCompression($this->complevel, $this->filetype($file));
273*ab8e5256SAndreas Gohr            }
274*ab8e5256SAndreas Gohr
275*ab8e5256SAndreas Gohr            if ($this->comptype === Archive::COMPRESS_GZIP) {
276*ab8e5256SAndreas Gohr                $this->fh = @gzopen($this->file, 'wb'.$this->complevel);
277*ab8e5256SAndreas Gohr            } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
278*ab8e5256SAndreas Gohr                $this->fh = @bzopen($this->file, 'w');
279*ab8e5256SAndreas Gohr            } else {
280*ab8e5256SAndreas Gohr                $this->fh = @fopen($this->file, 'wb');
281*ab8e5256SAndreas Gohr            }
282*ab8e5256SAndreas Gohr
283*ab8e5256SAndreas Gohr            if (!$this->fh) {
284*ab8e5256SAndreas Gohr                throw new ArchiveIOException('Could not open file for writing: '.$this->file);
285*ab8e5256SAndreas Gohr            }
286*ab8e5256SAndreas Gohr        }
287*ab8e5256SAndreas Gohr        $this->writeaccess = true;
288*ab8e5256SAndreas Gohr        $this->closed      = false;
289*ab8e5256SAndreas Gohr    }
290*ab8e5256SAndreas Gohr
291*ab8e5256SAndreas Gohr    /**
292*ab8e5256SAndreas Gohr     * Add a file to the current TAR archive using an existing file in the filesystem
293*ab8e5256SAndreas Gohr     *
294*ab8e5256SAndreas Gohr     * @param string $file path to the original file
295*ab8e5256SAndreas Gohr     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
296*ab8e5256SAndreas Gohr     * @throws ArchiveCorruptedException when the file changes while reading it, the archive will be corrupt and should be deleted
297*ab8e5256SAndreas Gohr     * @throws ArchiveIOException there was trouble reading the given file, it was not added
298*ab8e5256SAndreas Gohr     * @throws FileInfoException trouble reading file info, it was not added
299*ab8e5256SAndreas Gohr     */
300*ab8e5256SAndreas Gohr    public function addFile($file, $fileinfo = '')
301*ab8e5256SAndreas Gohr    {
302*ab8e5256SAndreas Gohr        if (is_string($fileinfo)) {
303*ab8e5256SAndreas Gohr            $fileinfo = FileInfo::fromPath($file, $fileinfo);
304*ab8e5256SAndreas Gohr        }
305*ab8e5256SAndreas Gohr
306*ab8e5256SAndreas Gohr        if ($this->closed) {
307*ab8e5256SAndreas Gohr            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
308*ab8e5256SAndreas Gohr        }
309*ab8e5256SAndreas Gohr
310*ab8e5256SAndreas Gohr        // create file header
311*ab8e5256SAndreas Gohr        $this->writeFileHeader($fileinfo);
312*ab8e5256SAndreas Gohr
313*ab8e5256SAndreas Gohr        // write data, but only if we have data to write.
314*ab8e5256SAndreas Gohr        // note: on Windows fopen() on a directory will fail, so we prevent
315*ab8e5256SAndreas Gohr        // errors on Windows by testing if we have data to write.
316*ab8e5256SAndreas Gohr        if (!$fileinfo->getIsdir() && $fileinfo->getSize() > 0) {
317*ab8e5256SAndreas Gohr            $read = 0;
318*ab8e5256SAndreas Gohr            $fp = @fopen($file, 'rb');
319*ab8e5256SAndreas Gohr            if (!$fp) {
320*ab8e5256SAndreas Gohr                throw new ArchiveIOException('Could not open file for reading: ' . $file);
321*ab8e5256SAndreas Gohr            }
322*ab8e5256SAndreas Gohr            while (!feof($fp)) {
323*ab8e5256SAndreas Gohr                // for performance reasons read bigger chunks at once
324*ab8e5256SAndreas Gohr                $data = fread($fp, self::READ_CHUNK_SIZE);
325*ab8e5256SAndreas Gohr                if ($data === false) {
326*ab8e5256SAndreas Gohr                    break;
327*ab8e5256SAndreas Gohr                }
328*ab8e5256SAndreas Gohr                if ($data === '') {
329*ab8e5256SAndreas Gohr                    break;
330*ab8e5256SAndreas Gohr                }
331*ab8e5256SAndreas Gohr                $dataLen = strlen($data);
332*ab8e5256SAndreas Gohr                $read += $dataLen;
333*ab8e5256SAndreas Gohr                // how much of data read fully fills 512-byte blocks?
334*ab8e5256SAndreas Gohr                $passLen = ($dataLen >> 9) << 9;
335*ab8e5256SAndreas Gohr                if ($passLen === $dataLen) {
336*ab8e5256SAndreas Gohr                    // all - just write the data
337*ab8e5256SAndreas Gohr                    $this->writebytes($data);
338*ab8e5256SAndreas Gohr                } else {
339*ab8e5256SAndreas Gohr                    // directly write what fills 512-byte blocks fully
340*ab8e5256SAndreas Gohr                    $this->writebytes(substr($data, 0, $passLen));
341*ab8e5256SAndreas Gohr                    // pad the reminder to 512 bytes
342*ab8e5256SAndreas Gohr                    $this->writebytes(pack("a512", substr($data, $passLen)));
343*ab8e5256SAndreas Gohr                }
344*ab8e5256SAndreas Gohr            }
345*ab8e5256SAndreas Gohr            fclose($fp);
346*ab8e5256SAndreas Gohr
347*ab8e5256SAndreas Gohr            if ($read != $fileinfo->getSize()) {
348*ab8e5256SAndreas Gohr                $this->close();
349*ab8e5256SAndreas Gohr                throw new ArchiveCorruptedException("The size of $file changed while reading, archive corrupted. read $read expected ".$fileinfo->getSize());
350*ab8e5256SAndreas Gohr            }
351*ab8e5256SAndreas Gohr        }
352*ab8e5256SAndreas Gohr
353*ab8e5256SAndreas Gohr        if(is_callable($this->callback)) {
354*ab8e5256SAndreas Gohr            call_user_func($this->callback, $fileinfo);
355*ab8e5256SAndreas Gohr        }
356*ab8e5256SAndreas Gohr    }
357*ab8e5256SAndreas Gohr
358*ab8e5256SAndreas Gohr    /**
359*ab8e5256SAndreas Gohr     * Add a file to the current TAR archive using the given $data as content
360*ab8e5256SAndreas Gohr     *
361*ab8e5256SAndreas Gohr     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
362*ab8e5256SAndreas Gohr     * @param string          $data     binary content of the file to add
363*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
364*ab8e5256SAndreas Gohr     */
365*ab8e5256SAndreas Gohr    public function addData($fileinfo, $data)
366*ab8e5256SAndreas Gohr    {
367*ab8e5256SAndreas Gohr        if (is_string($fileinfo)) {
368*ab8e5256SAndreas Gohr            $fileinfo = new FileInfo($fileinfo);
369*ab8e5256SAndreas Gohr        }
370*ab8e5256SAndreas Gohr
371*ab8e5256SAndreas Gohr        if ($this->closed) {
372*ab8e5256SAndreas Gohr            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
373*ab8e5256SAndreas Gohr        }
374*ab8e5256SAndreas Gohr
375*ab8e5256SAndreas Gohr        $len = strlen($data);
376*ab8e5256SAndreas Gohr        $fileinfo->setSize($len);
377*ab8e5256SAndreas Gohr        $this->writeFileHeader($fileinfo);
378*ab8e5256SAndreas Gohr
379*ab8e5256SAndreas Gohr        // write directly everything but the last block which needs padding
380*ab8e5256SAndreas Gohr        $passLen = ($len >> 9) << 9;
381*ab8e5256SAndreas Gohr        $this->writebytes(substr($data, 0, $passLen));
382*ab8e5256SAndreas Gohr        if ($passLen < $len) {
383*ab8e5256SAndreas Gohr            $this->writebytes(pack("a512", substr($data, $passLen, 512)));
384*ab8e5256SAndreas Gohr        }
385*ab8e5256SAndreas Gohr
386*ab8e5256SAndreas Gohr        if (is_callable($this->callback)) {
387*ab8e5256SAndreas Gohr            call_user_func($this->callback, $fileinfo);
388*ab8e5256SAndreas Gohr        }
389*ab8e5256SAndreas Gohr    }
390*ab8e5256SAndreas Gohr
391*ab8e5256SAndreas Gohr    /**
392*ab8e5256SAndreas Gohr     * Add the closing footer to the archive if in write mode, close all file handles
393*ab8e5256SAndreas Gohr     *
394*ab8e5256SAndreas Gohr     * After a call to this function no more data can be added to the archive, for
395*ab8e5256SAndreas Gohr     * read access no reading is allowed anymore
396*ab8e5256SAndreas Gohr     *
397*ab8e5256SAndreas Gohr     * "Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which
398*ab8e5256SAndreas Gohr     * consists of two 512 blocks of zero bytes"
399*ab8e5256SAndreas Gohr     *
400*ab8e5256SAndreas Gohr     * @link http://www.gnu.org/software/tar/manual/html_chapter/tar_8.html#SEC134
401*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
402*ab8e5256SAndreas Gohr     */
403*ab8e5256SAndreas Gohr    public function close()
404*ab8e5256SAndreas Gohr    {
405*ab8e5256SAndreas Gohr        if ($this->closed) {
406*ab8e5256SAndreas Gohr            return;
407*ab8e5256SAndreas Gohr        } // we did this already
408*ab8e5256SAndreas Gohr
409*ab8e5256SAndreas Gohr        // write footer
410*ab8e5256SAndreas Gohr        if ($this->writeaccess) {
411*ab8e5256SAndreas Gohr            $this->writebytes(pack("a512", ""));
412*ab8e5256SAndreas Gohr            $this->writebytes(pack("a512", ""));
413*ab8e5256SAndreas Gohr        }
414*ab8e5256SAndreas Gohr
415*ab8e5256SAndreas Gohr        // close file handles
416*ab8e5256SAndreas Gohr        if ($this->file) {
417*ab8e5256SAndreas Gohr            if ($this->comptype === Archive::COMPRESS_GZIP) {
418*ab8e5256SAndreas Gohr                gzclose($this->fh);
419*ab8e5256SAndreas Gohr            } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
420*ab8e5256SAndreas Gohr                bzclose($this->fh);
421*ab8e5256SAndreas Gohr            } else {
422*ab8e5256SAndreas Gohr                fclose($this->fh);
423*ab8e5256SAndreas Gohr            }
424*ab8e5256SAndreas Gohr
425*ab8e5256SAndreas Gohr            $this->file = '';
426*ab8e5256SAndreas Gohr            $this->fh   = 0;
427*ab8e5256SAndreas Gohr        }
428*ab8e5256SAndreas Gohr
429*ab8e5256SAndreas Gohr        $this->writeaccess = false;
430*ab8e5256SAndreas Gohr        $this->closed      = true;
431*ab8e5256SAndreas Gohr    }
432*ab8e5256SAndreas Gohr
433*ab8e5256SAndreas Gohr    /**
434*ab8e5256SAndreas Gohr     * Returns the created in-memory archive data
435*ab8e5256SAndreas Gohr     *
436*ab8e5256SAndreas Gohr     * This implicitly calls close() on the Archive
437*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
438*ab8e5256SAndreas Gohr     */
439*ab8e5256SAndreas Gohr    public function getArchive()
440*ab8e5256SAndreas Gohr    {
441*ab8e5256SAndreas Gohr        $this->close();
442*ab8e5256SAndreas Gohr
443*ab8e5256SAndreas Gohr        if ($this->comptype === Archive::COMPRESS_AUTO) {
444*ab8e5256SAndreas Gohr            $this->comptype = Archive::COMPRESS_NONE;
445*ab8e5256SAndreas Gohr        }
446*ab8e5256SAndreas Gohr
447*ab8e5256SAndreas Gohr        if ($this->comptype === Archive::COMPRESS_GZIP) {
448*ab8e5256SAndreas Gohr            return gzencode($this->memory, $this->complevel);
449*ab8e5256SAndreas Gohr        }
450*ab8e5256SAndreas Gohr        if ($this->comptype === Archive::COMPRESS_BZIP) {
451*ab8e5256SAndreas Gohr            return bzcompress($this->memory);
452*ab8e5256SAndreas Gohr        }
453*ab8e5256SAndreas Gohr        return $this->memory;
454*ab8e5256SAndreas Gohr    }
455*ab8e5256SAndreas Gohr
456*ab8e5256SAndreas Gohr    /**
457*ab8e5256SAndreas Gohr     * Save the created in-memory archive data
458*ab8e5256SAndreas Gohr     *
459*ab8e5256SAndreas Gohr     * Note: It more memory effective to specify the filename in the create() function and
460*ab8e5256SAndreas Gohr     * let the library work on the new file directly.
461*ab8e5256SAndreas Gohr     *
462*ab8e5256SAndreas Gohr     * @param string $file
463*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
464*ab8e5256SAndreas Gohr     * @throws ArchiveIllegalCompressionException
465*ab8e5256SAndreas Gohr     */
466*ab8e5256SAndreas Gohr    public function save($file)
467*ab8e5256SAndreas Gohr    {
468*ab8e5256SAndreas Gohr        if ($this->comptype === Archive::COMPRESS_AUTO) {
469*ab8e5256SAndreas Gohr            $this->setCompression($this->complevel, $this->filetype($file));
470*ab8e5256SAndreas Gohr        }
471*ab8e5256SAndreas Gohr
472*ab8e5256SAndreas Gohr        if (!@file_put_contents($file, $this->getArchive())) {
473*ab8e5256SAndreas Gohr            throw new ArchiveIOException('Could not write to file: '.$file);
474*ab8e5256SAndreas Gohr        }
475*ab8e5256SAndreas Gohr    }
476*ab8e5256SAndreas Gohr
477*ab8e5256SAndreas Gohr    /**
478*ab8e5256SAndreas Gohr     * Read from the open file pointer
479*ab8e5256SAndreas Gohr     *
480*ab8e5256SAndreas Gohr     * @param int $length bytes to read
481*ab8e5256SAndreas Gohr     * @return string
482*ab8e5256SAndreas Gohr     */
483*ab8e5256SAndreas Gohr    protected function readbytes($length)
484*ab8e5256SAndreas Gohr    {
485*ab8e5256SAndreas Gohr        if ($this->comptype === Archive::COMPRESS_GZIP) {
486*ab8e5256SAndreas Gohr            $ret = @gzread($this->fh, $length);
487*ab8e5256SAndreas Gohr        } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
488*ab8e5256SAndreas Gohr            $ret = @bzread($this->fh, $length);
489*ab8e5256SAndreas Gohr        } else {
490*ab8e5256SAndreas Gohr            $ret = @fread($this->fh, $length);
491*ab8e5256SAndreas Gohr        }
492*ab8e5256SAndreas Gohr        $this->position += strlen($ret);
493*ab8e5256SAndreas Gohr        return $ret;
494*ab8e5256SAndreas Gohr    }
495*ab8e5256SAndreas Gohr
496*ab8e5256SAndreas Gohr    /**
497*ab8e5256SAndreas Gohr     * Write to the open filepointer or memory
498*ab8e5256SAndreas Gohr     *
499*ab8e5256SAndreas Gohr     * @param string $data
500*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
501*ab8e5256SAndreas Gohr     * @return int number of bytes written
502*ab8e5256SAndreas Gohr     */
503*ab8e5256SAndreas Gohr    protected function writebytes($data)
504*ab8e5256SAndreas Gohr    {
505*ab8e5256SAndreas Gohr        if (!$this->file) {
506*ab8e5256SAndreas Gohr            $this->memory .= $data;
507*ab8e5256SAndreas Gohr            $written = strlen($data);
508*ab8e5256SAndreas Gohr        } elseif ($this->comptype === Archive::COMPRESS_GZIP) {
509*ab8e5256SAndreas Gohr            $written = @gzwrite($this->fh, $data);
510*ab8e5256SAndreas Gohr        } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
511*ab8e5256SAndreas Gohr            $written = @bzwrite($this->fh, $data);
512*ab8e5256SAndreas Gohr        } else {
513*ab8e5256SAndreas Gohr            $written = @fwrite($this->fh, $data);
514*ab8e5256SAndreas Gohr        }
515*ab8e5256SAndreas Gohr        if ($written === false) {
516*ab8e5256SAndreas Gohr            throw new ArchiveIOException('Failed to write to archive stream');
517*ab8e5256SAndreas Gohr        }
518*ab8e5256SAndreas Gohr        return $written;
519*ab8e5256SAndreas Gohr    }
520*ab8e5256SAndreas Gohr
521*ab8e5256SAndreas Gohr    /**
522*ab8e5256SAndreas Gohr     * Skip forward in the open file pointer
523*ab8e5256SAndreas Gohr     *
524*ab8e5256SAndreas Gohr     * This is basically a wrapper around seek() (and a workaround for bzip2)
525*ab8e5256SAndreas Gohr     *
526*ab8e5256SAndreas Gohr     * @param int $bytes seek to this position
527*ab8e5256SAndreas Gohr     */
528*ab8e5256SAndreas Gohr    protected function skipbytes($bytes)
529*ab8e5256SAndreas Gohr    {
530*ab8e5256SAndreas Gohr        if ($this->comptype === Archive::COMPRESS_GZIP) {
531*ab8e5256SAndreas Gohr            @gzseek($this->fh, $bytes, SEEK_CUR);
532*ab8e5256SAndreas Gohr        } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
533*ab8e5256SAndreas Gohr            // there is no seek in bzip2, we simply read on
534*ab8e5256SAndreas Gohr            // bzread allows to read a max of 8kb at once
535*ab8e5256SAndreas Gohr            while($bytes) {
536*ab8e5256SAndreas Gohr                $toread = min(8192, $bytes);
537*ab8e5256SAndreas Gohr                @bzread($this->fh, $toread);
538*ab8e5256SAndreas Gohr                $bytes -= $toread;
539*ab8e5256SAndreas Gohr            }
540*ab8e5256SAndreas Gohr        } else {
541*ab8e5256SAndreas Gohr            @fseek($this->fh, $bytes, SEEK_CUR);
542*ab8e5256SAndreas Gohr        }
543*ab8e5256SAndreas Gohr        $this->position += $bytes;
544*ab8e5256SAndreas Gohr    }
545*ab8e5256SAndreas Gohr
546*ab8e5256SAndreas Gohr    /**
547*ab8e5256SAndreas Gohr     * Write the given file meta data as header
548*ab8e5256SAndreas Gohr     *
549*ab8e5256SAndreas Gohr     * @param FileInfo $fileinfo
550*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
551*ab8e5256SAndreas Gohr     */
552*ab8e5256SAndreas Gohr    protected function writeFileHeader(FileInfo $fileinfo)
553*ab8e5256SAndreas Gohr    {
554*ab8e5256SAndreas Gohr        $this->writeRawFileHeader(
555*ab8e5256SAndreas Gohr            $fileinfo->getPath(),
556*ab8e5256SAndreas Gohr            $fileinfo->getUid(),
557*ab8e5256SAndreas Gohr            $fileinfo->getGid(),
558*ab8e5256SAndreas Gohr            $fileinfo->getMode(),
559*ab8e5256SAndreas Gohr            $fileinfo->getSize(),
560*ab8e5256SAndreas Gohr            $fileinfo->getMtime(),
561*ab8e5256SAndreas Gohr            $fileinfo->getIsdir() ? '5' : '0'
562*ab8e5256SAndreas Gohr        );
563*ab8e5256SAndreas Gohr    }
564*ab8e5256SAndreas Gohr
565*ab8e5256SAndreas Gohr    /**
566*ab8e5256SAndreas Gohr     * Write a file header to the stream
567*ab8e5256SAndreas Gohr     *
568*ab8e5256SAndreas Gohr     * @param string $name
569*ab8e5256SAndreas Gohr     * @param int $uid
570*ab8e5256SAndreas Gohr     * @param int $gid
571*ab8e5256SAndreas Gohr     * @param int $perm
572*ab8e5256SAndreas Gohr     * @param int $size
573*ab8e5256SAndreas Gohr     * @param int $mtime
574*ab8e5256SAndreas Gohr     * @param string $typeflag Set to '5' for directories
575*ab8e5256SAndreas Gohr     * @throws ArchiveIOException
576*ab8e5256SAndreas Gohr     */
577*ab8e5256SAndreas Gohr    protected function writeRawFileHeader($name, $uid, $gid, $perm, $size, $mtime, $typeflag = '')
578*ab8e5256SAndreas Gohr    {
579*ab8e5256SAndreas Gohr        // handle filename length restrictions
580*ab8e5256SAndreas Gohr        $prefix  = '';
581*ab8e5256SAndreas Gohr        $namelen = strlen($name);
582*ab8e5256SAndreas Gohr        if ($namelen > 100) {
583*ab8e5256SAndreas Gohr            $file = basename($name);
584*ab8e5256SAndreas Gohr            $dir  = dirname($name);
585*ab8e5256SAndreas Gohr            if (strlen($file) > 100 || strlen($dir) > 155) {
586*ab8e5256SAndreas Gohr                // we're still too large, let's use GNU longlink
587*ab8e5256SAndreas Gohr                $this->writeRawFileHeader('././@LongLink', 0, 0, 0, $namelen, 0, 'L');
588*ab8e5256SAndreas Gohr                for ($s = 0; $s < $namelen; $s += 512) {
589*ab8e5256SAndreas Gohr                    $this->writebytes(pack("a512", substr($name, $s, 512)));
590*ab8e5256SAndreas Gohr                }
591*ab8e5256SAndreas Gohr                $name = substr($name, 0, 100); // cut off name
592*ab8e5256SAndreas Gohr            } else {
593*ab8e5256SAndreas Gohr                // we're fine when splitting, use POSIX ustar
594*ab8e5256SAndreas Gohr                $prefix = $dir;
595*ab8e5256SAndreas Gohr                $name   = $file;
596*ab8e5256SAndreas Gohr            }
597*ab8e5256SAndreas Gohr        }
598*ab8e5256SAndreas Gohr
599*ab8e5256SAndreas Gohr        // values are needed in octal
600*ab8e5256SAndreas Gohr        $uid   = sprintf("%6s ", decoct($uid));
601*ab8e5256SAndreas Gohr        $gid   = sprintf("%6s ", decoct($gid));
602*ab8e5256SAndreas Gohr        $perm  = sprintf("%6s ", decoct($perm));
603*ab8e5256SAndreas Gohr        $size  = self::numberEncode($size, 12);
604*ab8e5256SAndreas Gohr        $mtime = self::numberEncode($mtime, 12);
605*ab8e5256SAndreas Gohr
606*ab8e5256SAndreas Gohr        $data_first = pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime);
607*ab8e5256SAndreas Gohr        $data_last  = pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, "");
608*ab8e5256SAndreas Gohr
609*ab8e5256SAndreas Gohr        for ($i = 0, $chks = 0; $i < 148; $i++) {
610*ab8e5256SAndreas Gohr            $chks += ord($data_first[$i]);
611*ab8e5256SAndreas Gohr        }
612*ab8e5256SAndreas Gohr
613*ab8e5256SAndreas Gohr        for ($i = 156, $chks += 256, $j = 0; $i < 512; $i++, $j++) {
614*ab8e5256SAndreas Gohr            $chks += ord($data_last[$j]);
615*ab8e5256SAndreas Gohr        }
616*ab8e5256SAndreas Gohr
617*ab8e5256SAndreas Gohr        $this->writebytes($data_first);
618*ab8e5256SAndreas Gohr
619*ab8e5256SAndreas Gohr        $chks = pack("a8", sprintf("%6s ", decoct($chks)));
620*ab8e5256SAndreas Gohr        $this->writebytes($chks.$data_last);
621*ab8e5256SAndreas Gohr    }
622*ab8e5256SAndreas Gohr
623*ab8e5256SAndreas Gohr    /**
624*ab8e5256SAndreas Gohr     * Decode the given tar file header
625*ab8e5256SAndreas Gohr     *
626*ab8e5256SAndreas Gohr     * @param string $block a 512 byte block containing the header data
627*ab8e5256SAndreas Gohr     * @return array|false returns false when this was a null block
628*ab8e5256SAndreas Gohr     * @throws ArchiveCorruptedException
629*ab8e5256SAndreas Gohr     */
630*ab8e5256SAndreas Gohr    protected function parseHeader($block)
631*ab8e5256SAndreas Gohr    {
632*ab8e5256SAndreas Gohr        if (!$block || strlen($block) != 512) {
633*ab8e5256SAndreas Gohr            throw new ArchiveCorruptedException('Unexpected length of header');
634*ab8e5256SAndreas Gohr        }
635*ab8e5256SAndreas Gohr
636*ab8e5256SAndreas Gohr        // null byte blocks are ignored
637*ab8e5256SAndreas Gohr        if(trim($block) === '') return false;
638*ab8e5256SAndreas Gohr
639*ab8e5256SAndreas Gohr        for ($i = 0, $chks = 0; $i < 148; $i++) {
640*ab8e5256SAndreas Gohr            $chks += ord($block[$i]);
641*ab8e5256SAndreas Gohr        }
642*ab8e5256SAndreas Gohr
643*ab8e5256SAndreas Gohr        for ($i = 156, $chks += 256; $i < 512; $i++) {
644*ab8e5256SAndreas Gohr            $chks += ord($block[$i]);
645*ab8e5256SAndreas Gohr        }
646*ab8e5256SAndreas Gohr
647*ab8e5256SAndreas Gohr        $header = @unpack(
648*ab8e5256SAndreas Gohr            "a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix",
649*ab8e5256SAndreas Gohr            $block
650*ab8e5256SAndreas Gohr        );
651*ab8e5256SAndreas Gohr        if (!$header) {
652*ab8e5256SAndreas Gohr            throw new ArchiveCorruptedException('Failed to parse header');
653*ab8e5256SAndreas Gohr        }
654*ab8e5256SAndreas Gohr
655*ab8e5256SAndreas Gohr        $return['checksum'] = OctDec(trim($header['checksum']));
656*ab8e5256SAndreas Gohr        if ($return['checksum'] != $chks) {
657*ab8e5256SAndreas Gohr            throw new ArchiveCorruptedException('Header does not match its checksum');
658*ab8e5256SAndreas Gohr        }
659*ab8e5256SAndreas Gohr
660*ab8e5256SAndreas Gohr        $return['filename'] = trim($header['filename']);
661*ab8e5256SAndreas Gohr        $return['perm']     = OctDec(trim($header['perm']));
662*ab8e5256SAndreas Gohr        $return['uid']      = OctDec(trim($header['uid']));
663*ab8e5256SAndreas Gohr        $return['gid']      = OctDec(trim($header['gid']));
664*ab8e5256SAndreas Gohr        $return['size']     = self::numberDecode($header['size']);
665*ab8e5256SAndreas Gohr        $return['mtime']    = self::numberDecode($header['mtime']);
666*ab8e5256SAndreas Gohr        $return['typeflag'] = $header['typeflag'];
667*ab8e5256SAndreas Gohr        $return['link']     = trim($header['link']);
668*ab8e5256SAndreas Gohr        $return['uname']    = trim($header['uname']);
669*ab8e5256SAndreas Gohr        $return['gname']    = trim($header['gname']);
670*ab8e5256SAndreas Gohr
671*ab8e5256SAndreas Gohr        // Handle ustar Posix compliant path prefixes
672*ab8e5256SAndreas Gohr        if (trim($header['prefix'])) {
673*ab8e5256SAndreas Gohr            $return['filename'] = trim($header['prefix']).'/'.$return['filename'];
674*ab8e5256SAndreas Gohr        }
675*ab8e5256SAndreas Gohr
676*ab8e5256SAndreas Gohr        // Handle Long-Link entries from GNU Tar
677*ab8e5256SAndreas Gohr        if ($return['typeflag'] == 'L') {
678*ab8e5256SAndreas Gohr            // following data block(s) is the filename
679*ab8e5256SAndreas Gohr            $filename = trim($this->readbytes(ceil($return['size'] / 512) * 512));
680*ab8e5256SAndreas Gohr            // next block is the real header
681*ab8e5256SAndreas Gohr            $block  = $this->readbytes(512);
682*ab8e5256SAndreas Gohr            $return = $this->parseHeader($block);
683*ab8e5256SAndreas Gohr            // overwrite the filename
684*ab8e5256SAndreas Gohr            $return['filename'] = $filename;
685*ab8e5256SAndreas Gohr        }
686*ab8e5256SAndreas Gohr
687*ab8e5256SAndreas Gohr        return $return;
688*ab8e5256SAndreas Gohr    }
689*ab8e5256SAndreas Gohr
690*ab8e5256SAndreas Gohr    /**
691*ab8e5256SAndreas Gohr     * Creates a FileInfo object from the given parsed header
692*ab8e5256SAndreas Gohr     *
693*ab8e5256SAndreas Gohr     * @param $header
694*ab8e5256SAndreas Gohr     * @return FileInfo
695*ab8e5256SAndreas Gohr     */
696*ab8e5256SAndreas Gohr    protected function header2fileinfo($header)
697*ab8e5256SAndreas Gohr    {
698*ab8e5256SAndreas Gohr        $fileinfo = new FileInfo();
699*ab8e5256SAndreas Gohr        $fileinfo->setPath($header['filename']);
700*ab8e5256SAndreas Gohr        $fileinfo->setMode($header['perm']);
701*ab8e5256SAndreas Gohr        $fileinfo->setUid($header['uid']);
702*ab8e5256SAndreas Gohr        $fileinfo->setGid($header['gid']);
703*ab8e5256SAndreas Gohr        $fileinfo->setSize($header['size']);
704*ab8e5256SAndreas Gohr        $fileinfo->setMtime($header['mtime']);
705*ab8e5256SAndreas Gohr        $fileinfo->setOwner($header['uname']);
706*ab8e5256SAndreas Gohr        $fileinfo->setGroup($header['gname']);
707*ab8e5256SAndreas Gohr        $fileinfo->setIsdir((bool) $header['typeflag']);
708*ab8e5256SAndreas Gohr
709*ab8e5256SAndreas Gohr        return $fileinfo;
710*ab8e5256SAndreas Gohr    }
711*ab8e5256SAndreas Gohr
712*ab8e5256SAndreas Gohr    /**
713*ab8e5256SAndreas Gohr     * Checks if the given compression type is available and throws an exception if not
714*ab8e5256SAndreas Gohr     *
715*ab8e5256SAndreas Gohr     * @param $comptype
716*ab8e5256SAndreas Gohr     * @throws ArchiveIllegalCompressionException
717*ab8e5256SAndreas Gohr     */
718*ab8e5256SAndreas Gohr    protected function compressioncheck($comptype)
719*ab8e5256SAndreas Gohr    {
720*ab8e5256SAndreas Gohr        if ($comptype === Archive::COMPRESS_GZIP && !function_exists('gzopen')) {
721*ab8e5256SAndreas Gohr            throw new ArchiveIllegalCompressionException('No gzip support available');
722*ab8e5256SAndreas Gohr        }
723*ab8e5256SAndreas Gohr
724*ab8e5256SAndreas Gohr        if ($comptype === Archive::COMPRESS_BZIP && !function_exists('bzopen')) {
725*ab8e5256SAndreas Gohr            throw new ArchiveIllegalCompressionException('No bzip2 support available');
726*ab8e5256SAndreas Gohr        }
727*ab8e5256SAndreas Gohr    }
728*ab8e5256SAndreas Gohr
729*ab8e5256SAndreas Gohr    /**
730*ab8e5256SAndreas Gohr     * Guesses the wanted compression from the given file
731*ab8e5256SAndreas Gohr     *
732*ab8e5256SAndreas Gohr     * Uses magic bytes for existing files, the file extension otherwise
733*ab8e5256SAndreas Gohr     *
734*ab8e5256SAndreas Gohr     * You don't need to call this yourself. It's used when you pass Archive::COMPRESS_AUTO somewhere
735*ab8e5256SAndreas Gohr     *
736*ab8e5256SAndreas Gohr     * @param string $file
737*ab8e5256SAndreas Gohr     * @return int
738*ab8e5256SAndreas Gohr     */
739*ab8e5256SAndreas Gohr    public function filetype($file)
740*ab8e5256SAndreas Gohr    {
741*ab8e5256SAndreas Gohr        // for existing files, try to read the magic bytes
742*ab8e5256SAndreas Gohr        if(file_exists($file) && is_readable($file) && filesize($file) > 5) {
743*ab8e5256SAndreas Gohr            $fh = @fopen($file, 'rb');
744*ab8e5256SAndreas Gohr            if(!$fh) return false;
745*ab8e5256SAndreas Gohr            $magic = fread($fh, 5);
746*ab8e5256SAndreas Gohr            fclose($fh);
747*ab8e5256SAndreas Gohr
748*ab8e5256SAndreas Gohr            if(strpos($magic, "\x42\x5a") === 0) return Archive::COMPRESS_BZIP;
749*ab8e5256SAndreas Gohr            if(strpos($magic, "\x1f\x8b") === 0) return Archive::COMPRESS_GZIP;
750*ab8e5256SAndreas Gohr        }
751*ab8e5256SAndreas Gohr
752*ab8e5256SAndreas Gohr        // otherwise rely on file name
753*ab8e5256SAndreas Gohr        $file = strtolower($file);
754*ab8e5256SAndreas Gohr        if (substr($file, -3) == '.gz' || substr($file, -4) == '.tgz') {
755*ab8e5256SAndreas Gohr            return Archive::COMPRESS_GZIP;
756*ab8e5256SAndreas Gohr        } elseif (substr($file, -4) == '.bz2' || substr($file, -4) == '.tbz') {
757*ab8e5256SAndreas Gohr            return Archive::COMPRESS_BZIP;
758*ab8e5256SAndreas Gohr        }
759*ab8e5256SAndreas Gohr
760*ab8e5256SAndreas Gohr        return Archive::COMPRESS_NONE;
761*ab8e5256SAndreas Gohr    }
762*ab8e5256SAndreas Gohr
763*ab8e5256SAndreas Gohr    /**
764*ab8e5256SAndreas Gohr     * Decodes numeric values according to the
765*ab8e5256SAndreas Gohr     * https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions
766*ab8e5256SAndreas Gohr     * (basically with support for big numbers)
767*ab8e5256SAndreas Gohr     *
768*ab8e5256SAndreas Gohr     * @param string $field
769*ab8e5256SAndreas Gohr     * $return int
770*ab8e5256SAndreas Gohr     */
771*ab8e5256SAndreas Gohr    static public function numberDecode($field)
772*ab8e5256SAndreas Gohr    {
773*ab8e5256SAndreas Gohr        $firstByte = ord(substr($field, 0, 1));
774*ab8e5256SAndreas Gohr        if ($firstByte === 255) {
775*ab8e5256SAndreas Gohr            $value = -1 << (8 * strlen($field));
776*ab8e5256SAndreas Gohr            $shift = 0;
777*ab8e5256SAndreas Gohr            for ($i = strlen($field) - 1; $i >= 0; $i--) {
778*ab8e5256SAndreas Gohr                $value += ord(substr($field, $i, 1)) << $shift;
779*ab8e5256SAndreas Gohr                $shift += 8;
780*ab8e5256SAndreas Gohr            }
781*ab8e5256SAndreas Gohr        } elseif ($firstByte === 128) {
782*ab8e5256SAndreas Gohr            $value = 0;
783*ab8e5256SAndreas Gohr            $shift = 0;
784*ab8e5256SAndreas Gohr            for ($i = strlen($field) - 1; $i > 0; $i--) {
785*ab8e5256SAndreas Gohr                $value += ord(substr($field, $i, 1)) << $shift;
786*ab8e5256SAndreas Gohr                $shift += 8;
787*ab8e5256SAndreas Gohr            }
788*ab8e5256SAndreas Gohr        } else {
789*ab8e5256SAndreas Gohr            $value = octdec(trim($field));
790*ab8e5256SAndreas Gohr        }
791*ab8e5256SAndreas Gohr        return $value;
792*ab8e5256SAndreas Gohr    }
793*ab8e5256SAndreas Gohr
794*ab8e5256SAndreas Gohr    /**
795*ab8e5256SAndreas Gohr     * Encodes numeric values according to the
796*ab8e5256SAndreas Gohr     * https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions
797*ab8e5256SAndreas Gohr     * (basically with support for big numbers)
798*ab8e5256SAndreas Gohr     *
799*ab8e5256SAndreas Gohr     * @param int $value
800*ab8e5256SAndreas Gohr     * @param int $length field length
801*ab8e5256SAndreas Gohr     * @return string
802*ab8e5256SAndreas Gohr     */
803*ab8e5256SAndreas Gohr    static public function numberEncode($value, $length)
804*ab8e5256SAndreas Gohr    {
805*ab8e5256SAndreas Gohr        // old implementations leave last byte empty
806*ab8e5256SAndreas Gohr        // octal encoding encodes three bits per byte
807*ab8e5256SAndreas Gohr        $maxValue = 1 << (($length - 1) * 3);
808*ab8e5256SAndreas Gohr        if ($value < 0) {
809*ab8e5256SAndreas Gohr            // PHP already stores integers as 2's complement
810*ab8e5256SAndreas Gohr            $value = pack(PHP_INT_SIZE === 8 ? 'J' : 'N', (int) $value);
811*ab8e5256SAndreas Gohr            $encoded = str_repeat(chr(255), max(1, $length - PHP_INT_SIZE));
812*ab8e5256SAndreas Gohr            $encoded .= substr($value, max(0, PHP_INT_SIZE - $length + 1));
813*ab8e5256SAndreas Gohr        } elseif ($value >= $maxValue) {
814*ab8e5256SAndreas Gohr            $value = pack(PHP_INT_SIZE === 8 ? 'J' : 'N', (int) $value);
815*ab8e5256SAndreas Gohr            $encoded = chr(128) . str_repeat(chr(0), max(0, $length - PHP_INT_SIZE - 1));
816*ab8e5256SAndreas Gohr            $encoded .= substr($value, max(0, PHP_INT_SIZE - $length + 1));
817*ab8e5256SAndreas Gohr        } else {
818*ab8e5256SAndreas Gohr            $encoded = sprintf("%" . ($length - 1) . "s ", decoct($value));
819*ab8e5256SAndreas Gohr        }
820*ab8e5256SAndreas Gohr        return $encoded;
821*ab8e5256SAndreas Gohr    }
822*ab8e5256SAndreas Gohr}
823*ab8e5256SAndreas Gohr
824