From bee9f377bc547c99fe99b4e38199cb92cf668554 Mon Sep 17 00:00:00 2001
From: Andreas Gohr <andi@splitbrain.org>
Date: Sat, 3 Nov 2012 17:54:02 +0100
Subject: [PATCH] Completely rewritten Tar library

This new class is only losely based on our previous library. The
whole API was changed to make it more flexible and memory saving.

Some fisrt unit tests are included
---
 _test/tests/inc/tar.test.php             | 134 +++++
 _test/tests/inc/tar/foobar/testdata2.txt |   1 +
 _test/tests/inc/tar/test.tar             | Bin 0 -> 10240 bytes
 _test/tests/inc/tar/test.tbz             | Bin 0 -> 217 bytes
 _test/tests/inc/tar/test.tgz             | Bin 0 -> 220 bytes
 _test/tests/inc/tar/testdata1.txt        |   1 +
 _test/tests/inc/tarlib.test.php          |   0
 inc/Tar.class.php                        | 614 +++++++++++++++++++++++
 inc/load.php                             |   1 +
 9 files changed, 751 insertions(+)
 create mode 100644 _test/tests/inc/tar.test.php
 create mode 100644 _test/tests/inc/tar/foobar/testdata2.txt
 create mode 100644 _test/tests/inc/tar/test.tar
 create mode 100644 _test/tests/inc/tar/test.tbz
 create mode 100644 _test/tests/inc/tar/test.tgz
 create mode 100644 _test/tests/inc/tar/testdata1.txt
 delete mode 100644 _test/tests/inc/tarlib.test.php
 create mode 100644 inc/Tar.class.php

diff --git a/_test/tests/inc/tar.test.php b/_test/tests/inc/tar.test.php
new file mode 100644
index 000000000..706516d9e
--- /dev/null
+++ b/_test/tests/inc/tar.test.php
@@ -0,0 +1,134 @@
+<?php
+
+class Tar_TestCase extends DokuWikiTest {
+
+    /**
+     * simple test that checks that the given filenames and contents can be grepped from
+     * the uncompressed tar stream
+     *
+     * No check for format correctness
+     */
+    public function test_createdynamic(){
+        $tar = new Tar();
+
+        $dir = dirname(__FILE__).'/tar';
+
+        $tar->create();
+        $tar->AddFile("$dir/testdata1.txt");
+        $tar->AddFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
+        $tar->addData('another/testdata3.txt', 'testcontent3');
+
+        $data = $tar->getArchive();
+
+        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content in TAR');
+        $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content in TAR');
+        $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content in TAR');
+
+        $this->assertTrue(strpos($data, "$dir/testdata1.txt") !== false, 'Path in TAR');
+        $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in TAR');
+        $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in TAR');
+
+        $this->assertTrue(strpos($data, "$dir/foobar/testdata2.txt") === false, 'Path not in TAR');
+        $this->assertTrue(strpos($data, "foobar") === false, 'Path not in TAR');
+    }
+
+    /**
+     * simple test that checks that the given filenames and contents can be grepped from the
+     * uncompressed tar file
+     *
+     * No check for format correctness
+     */
+    public function test_createfile(){
+        $tar = new Tar();
+
+        $dir = dirname(__FILE__).'/tar';
+        $tmp = tempnam(sys_get_temp_dir(), 'dwtartest');
+
+        $tar->create($tmp, Tar::COMPRESS_NONE);
+        $tar->AddFile("$dir/testdata1.txt");
+        $tar->AddFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
+        $tar->addData('another/testdata3.txt', 'testcontent3');
+        $tar->close();
+
+        $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
+        $data = file_get_contents($tmp);
+
+        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content in TAR');
+        $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content in TAR');
+        $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content in TAR');
+
+        $this->assertTrue(strpos($data, "$dir/testdata1.txt") !== false, 'Path in TAR');
+        $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in TAR');
+        $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in TAR');
+
+        $this->assertTrue(strpos($data, "$dir/foobar/testdata2.txt") === false, 'Path not in TAR');
+        $this->assertTrue(strpos($data, "foobar") === false, 'Path not in TAR');
+
+        @unlink($tmp);
+    }
+
+    /**
+     * List the contents of the prebuilt TAR files
+     */
+    public function test_tarcontent(){
+        $dir = dirname(__FILE__).'/tar';
+
+        foreach(array('tar','tgz','tbz') as $ext){
+            $tar  = new Tar();
+            $file = "$dir/test.$ext";
+
+            $tar->open($file);
+            $content = $tar->contents();
+
+            $this->assertCount(4, $content, "Contents of $file");
+            $this->assertEquals('tar/testdata1.txt', $content[1]['filename'], "Contents of $file");
+            $this->assertEquals(13, $content[1]['size'], "Contents of $file");
+
+            $this->assertEquals('tar/foobar/testdata2.txt', $content[3]['filename'], "Contents of $file");
+            $this->assertEquals(13, $content[1]['size'], "Contents of $file");
+        }
+    }
+
+    /**
+     * Extract the prebuilt tar files
+     */
+    public function test_tarextract(){
+        $dir = dirname(__FILE__).'/tar';
+        $out = sys_get_temp_dir().'/dwtartest'.md5(time());
+
+        foreach(array('tar', 'tgz', 'tbz') as $ext){
+            $tar  = new Tar();
+            $file = "$dir/test.$ext";
+
+            $tar->open($file);
+            $tar->extract($out);
+
+            clearstatcache();
+
+            $this->assertFileExists($out.'/tar/testdata1.txt', "Extracted $file");
+            $this->assertEquals(13, filesize($out.'/tar/testdata1.txt'), "Extracted $file");
+
+            $this->assertFileExists($out.'/tar/foobar/testdata2.txt', "Extracted $file");
+            $this->assertEquals(13, filesize($out.'/tar/foobar/testdata2.txt'), "Extracted $file");
+
+            TestUtils::rdelete($out);
+        }
+
+    }
+
+    /**
+     * Check the extension to compression guesser
+     */
+    public function test_filetype(){
+        $tar  = new Tar();
+        $this->assertEquals(Tar::COMPRESS_NONE, $tar->filetype('foo'));
+        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tgz'));
+        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tGZ'));
+        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tar.GZ'));
+        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tar.gz'));
+        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tbz'));
+        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tBZ'));
+        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tar.BZ2'));
+        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tar.bz2'));
+    }
+}
\ No newline at end of file
diff --git a/_test/tests/inc/tar/foobar/testdata2.txt b/_test/tests/inc/tar/foobar/testdata2.txt
new file mode 100644
index 000000000..a7db15771
--- /dev/null
+++ b/_test/tests/inc/tar/foobar/testdata2.txt
@@ -0,0 +1 @@
+testcontent2
diff --git a/_test/tests/inc/tar/test.tar b/_test/tests/inc/tar/test.tar
new file mode 100644
index 0000000000000000000000000000000000000000..931866b0ba0f279fab0b68e6a27e988860e41dc4
GIT binary patch
literal 10240
zcmeIz(F%ev6b9g3_Y`}AJV!V8JSz>lQP30g_?=lnVMsv>s(&{V6}Nr=Jd-#SCUYZg
z!F!_}2T!Rdc5dp^wKl^gIbU)eSl?sv#K!u}^O`<?7$pwYn%Fh_rST49JB&*&eCIP=
zd?D>9O_a!MIZB3Qq=4&l-jDOw%<uEBxFTaI!(3*c{P+Ad`)%Jz+X=5`IJoia0_p$X
z_5CL2F_8ZMr|Z89TK~07)TsZT*n2nmUyp(E-N(R7^?%CWInw-5|MTyF6#Aw9m#F{w
uy)gm=AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwx`TmmO_reCc9

literal 0
HcmV?d00001

diff --git a/_test/tests/inc/tar/test.tbz b/_test/tests/inc/tar/test.tbz
new file mode 100644
index 0000000000000000000000000000000000000000..5a737401907117fc2bfda39d27f73aed5892adac
GIT binary patch
literal 217
zcmV;~04D!JT4*^jL0KkKSu)~jd;kFw|AfMl06;(i|9}8Mf&_mdo<INrKnO4Z_5p)U
z8fk>Y$$){T2}J>*13&-(6;mQLG$xv9qtx_(+KiuS9TF829Rvhw2#Ul+1bL+{14Lq2
zt2>Ie7al|uB)^?#v8g3(RV?(ma)r$E(@P8tWRr2bg?anS6MV@WS+Xi+vt*k{ZKWXO
zj7*$ZtW>rn(rvkcg5;G%43I(b1bsDB)P{r|7#=58&aGBaW1;#a{%>I3b?@C%1h_jp
T9nMZ=5Ak;-Q-uiwE+&`2Za`N2

literal 0
HcmV?d00001

diff --git a/_test/tests/inc/tar/test.tgz b/_test/tests/inc/tar/test.tgz
new file mode 100644
index 0000000000000000000000000000000000000000..b0031964934865544c7c8537740a7995f99c882f
GIT binary patch
literal 220
zcmV<203-h&iwFQ#E0s_H1MSsK3c@fD1>mebMNVL6+Wed++6r!3Xh-n)CRL%L2n7=n
z-?s^cflPU1M#5OB%qa5Krjxy;`J0WI&l|-x8C$dS9P5}YvC=koW65cfFlwzr-yYxB
zb>mc`p|ax7SJF+=1_`{Cvt+m<1?;YQzs^5q{+NHm4Jl0-dU^8i`N!<{LoZz~y!m!B
zP+WTdhhf;|Tm!4-zwz<?$1;ccU&12)`x>Y}y#`jz|2cnS$YK7Mu$ccf=05-c00000
W000000002M@45hgL7JNYC;$NZ_i5h%

literal 0
HcmV?d00001

diff --git a/_test/tests/inc/tar/testdata1.txt b/_test/tests/inc/tar/testdata1.txt
new file mode 100644
index 000000000..ac65bb32e
--- /dev/null
+++ b/_test/tests/inc/tar/testdata1.txt
@@ -0,0 +1 @@
+testcontent1
diff --git a/_test/tests/inc/tarlib.test.php b/_test/tests/inc/tarlib.test.php
deleted file mode 100644
index e69de29bb..000000000
diff --git a/inc/Tar.class.php b/inc/Tar.class.php
new file mode 100644
index 000000000..7f5e5af4a
--- /dev/null
+++ b/inc/Tar.class.php
@@ -0,0 +1,614 @@
+<?php
+/**
+ * This class allows the extraction of existing and the creation of new Unix TAR archives.
+ * To keep things simple, the modification of existing archives is not supported. It handles
+ * uncompressed, gzip and bzip2 compressed tar files.
+ *
+ * To list the contents of an existing TAR archive, open() it and use contents() on it:
+ *
+ *     $tar = new Tar();
+ *     $tar->open('myfile.tgz');
+ *     $toc = $tar->contents();
+ *     print_r($toc);
+ *
+ * To extract the contents of an existing TAR archive, open() it and use extract() on it:
+ *
+ *     $tar = new Tar();
+ *     $tar->open('myfile.tgz');
+ *     $tar->extract(/tmp);
+ *
+ * To create a new TAR archive directly on the filesystem (low memory requirements), create() it,
+ * add*() files and close() it:
+ *
+ *      $tar = new Tar();
+ *      $tar->create('myfile.tgz');
+ *      $tar->addFile(...);
+ *      $tar->addData(...);
+ *      ...
+ *      $tar->close();
+ *
+ * To create a TAR archive directly in memory, create() it, add*() files and then either save()
+ * or getData() it:
+ *
+ *      $tar = new Tar();
+ *      $tar->create();
+ *      $tar->addFile(...);
+ *      $tar->addData(...);
+ *      ...
+ *      $tar->save('myfile.tgz'); // compresses and saves it
+ *      echo $tar->getArchive(Tar::COMPRESS_GZIP); // compresses and returns it
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Bouchon <tarlib@bouchon.org> (Maxg)
+ * @license GPL 2
+ */
+class Tar {
+
+    const COMPRESS_AUTO = 0;
+    const COMPRESS_NONE = 1;
+    const COMPRESS_GZIP = 2;
+    const COMPRESS_BZIP = 3;
+
+    protected $file = '';
+    protected $comptype = Tar::COMPRESS_AUTO;
+    protected $fh;
+    protected $memory = '';
+    protected $closed = true;
+    protected $writeaccess = false;
+
+    /**
+     * Open an existing TAR file for reading
+     *
+     * @param string $file
+     * @param int    $comptype
+     * @throws TarIOException
+     */
+    public function open($file, $comptype = Tar::COMPRESS_AUTO) {
+        // determine compression
+        if($comptype == Tar::COMPRESS_AUTO) $comptype = $this->filetype($file);
+        $this->compressioncheck($comptype);
+
+        $this->comptype = $comptype;
+        $this->file     = $file;
+
+        if($this->comptype === Tar::COMPRESS_GZIP) {
+            $this->fh = @gzopen($this->file, 'rb');
+        } elseif($this->comptype === Tar::COMPRESS_BZIP) {
+            $this->fh = @bzopen($this->file, 'r');
+        } else {
+            $this->fh = @fopen($this->file, 'rb');
+        }
+
+        if(!$this->fh) throw(new TarIOException('Could not open file for reading: '.$this->file));
+        $this->closed = false;
+    }
+
+    /**
+     * Read the contents of a TAR archive
+     *
+     * This function lists the files stored in the archive, and returns an indexed array of associative
+     * arrays containing for each file the following information:
+     *
+     * checksum    Tar Checksum of the file
+     * filename    The full name of the stored file (up to 100 c.)
+     * mode        UNIX permissions in DECIMAL, not octal
+     * uid         The Owner ID
+     * gid         The Group ID
+     * size        Uncompressed filesize
+     * mtime       Timestamp of last modification
+     * typeflag    Empty for files, set for folders
+     * link        Is it a symlink?
+     * uname       Owner name
+     * gname       Group name
+     *
+     * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
+     * Reopen the file with open() again if you want to do additional operations
+     */
+    public function contents() {
+        if($this->closed || !$this->file) throw(new TarIOException('Can not read from a closed archive'));
+
+        $result = Array();
+        while($read = $this->readbytes(512)) {
+            $header = $this->parseHeader($read);
+            if(!is_array($header)) continue;
+
+            $this->skipbytes(ceil($header['size'] / 512) * 512, 1);
+            $result[] = $header;
+        }
+
+        $this->close();
+        return $result;
+    }
+
+    /**
+     * Extract an existing TAR archive
+     *
+     * The $strip parameter allows you to strip a certain number of path components from the filenames
+     * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
+     * an integer is passed as $strip.
+     * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
+     * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
+     *
+     * By default this will extract all files found in the archive. You can restrict the output using the $include
+     * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
+     * $include is set only files that match this expression will be extracted. Files that match the $exclude
+     * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
+     * stripped filenames as described above.
+     *
+     * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
+     * Reopen the file with open() again if you want to do additional operations
+     *
+     * @param string     $outdir  the target directory for extracting
+     * @param int|string $strip   either the number of path components or a fixed prefix to strip
+     * @param string     $exclude a regular expression of files to exclude
+     * @param string     $include a regular expression of files to include
+     * @throws TarIOException
+     * @return array
+     */
+    function extract($outdir, $strip='', $exclude='', $include='') {
+        if($this->closed || !$this->file) throw(new TarIOException('Can not read from a closed archive'));
+
+        $outdir = rtrim($outdir,'/');
+        io_mkdir_p($outdir);
+        $striplen = strlen($strip);
+
+        $extracted = array();
+
+        while($dat = $this->readbytes(512)) {
+            // read the file header
+            $header = $this->parseHeader($dat);
+            if(!is_array($header)) continue;
+            if(!$header['filename']) continue;
+
+            // strip prefix
+            $filename = $this->cleanPath($header['filename']);
+            if(is_int($strip)) {
+                // if $strip is an integer we strip this many path components
+                $parts = explode('/',$filename);
+                if(!$header['typeflag']){
+                    $base = array_pop($parts); // keep filename itself
+                }else{
+                    $base = '';
+                }
+                $filename = join('/',array_slice($parts,$strip));
+                if($base) $filename .= "/$base";
+            }else{
+                // ifstrip is a string, we strip a prefix here
+                if(substr($filename,0,$striplen) == $strip) $filename = substr($filename,$striplen);
+            }
+
+            // check if this should be extracted
+            $extract = true;
+            if(!$filename){
+                $extract = false;
+            }else{
+                if($include){
+                    if(preg_match($include, $filename)){
+                        $extract = true;
+                    }else{
+                        $extract = false;
+                    }
+                }
+                if($exclude && preg_match($exclude, $filename)){
+                    $extract = false;
+                }
+            }
+
+            // Now do the extraction (or not)
+            if($extract) {
+                $extracted[] = $header;
+
+                $output    = "$outdir/$filename";
+                $directory = ($header['typeflag']) ? $output : dirname($output);
+                io_mkdir_p($directory);
+
+                // is this a file?
+                if(!$header['typeflag']){
+                    $fp = @fopen($output, "wb");
+                    if(!$fp) throw(new TarIOException('Could not open file for writing: '.$output));
+
+                    $size = floor($header['size'] / 512);
+                    for($i = 0; $i < $size; $i++) {
+                        fwrite($fp, $this->readbytes(512), 512);
+                    }
+                    if(($header['size'] % 512) != 0) fwrite($fp, $this->readbytes(512), $header['size'] % 512);
+
+                    fclose($fp);
+                    touch($output, $header['mtime']);
+                    chmod($output, $header['perm']);
+                }else{
+                    $this->skipbytes(ceil($header['size'] / 512) * 512); // the size is usually 0 for directories
+                }
+            }else{
+                $this->skipbytes(ceil($header['size'] / 512) * 512);
+            }
+        }
+
+        $this->close();
+        return $extracted;
+    }
+
+    /**
+     * Create a new TAR file
+     *
+     * If $file is empty, the tar file will be created in memory
+     *
+     * @param string $file
+     * @param int    $comptype
+     * @param int    $complevel
+     * @throws TarIOException
+     * @throws TarIllegalCompressionException
+     */
+    public function create($file = '', $comptype = Tar::COMPRESS_AUTO, $complevel = 9) {
+        // determine compression
+        if($comptype == Tar::COMPRESS_AUTO) $comptype = $this->filetype($file);
+        $this->compressioncheck($comptype);
+
+        $this->comptype = $comptype;
+        $this->file     = $file;
+        $this->memory   = '';
+        $this->fh       = 0;
+
+        if($this->file) {
+            if($this->comptype === Tar::COMPRESS_GZIP) {
+                $this->fh = @gzopen($this->file, 'wb'.$complevel);
+            } elseif($this->comptype === Tar::COMPRESS_BZIP) {
+                $this->fh = @bzopen($this->file, 'w');
+            } else {
+                $this->fh = @fopen($this->file, 'wb');
+            }
+
+            if(!$this->fh) throw(new TarIOException('Could not open file for writing: '.$this->file));
+        }
+        $this->writeaccess = false;
+        $this->closed = false;
+    }
+
+    /**
+     * Add a file to the current TAR archive using an existing file in the filesystem
+     *
+     * @todo handle directory adding
+     * @param string $file the original file
+     * @param string $name the name to use for the file in the archive
+     * @throws TarBadFilename
+     * @throws TarIOException
+     */
+    public function addFile($file, $name = '') {
+        if($this->closed) throw(new TarIOException('Archive has been closed, files can no longer be added'));
+
+        if(!$name) $name = $file;
+        $name = $this->cleanPath($name);
+
+        // FIXME ustar should support up 256 chars
+        if(strlen($name) > 99) throw(new TarBadFilename('Filenames may not exceed 99 bytes: '.$name));
+
+        $fp = fopen($file, 'rb');
+        if(!$fp) throw(new TarIOException('Could not open file for reading: '.$file));
+
+        // create file header and copy all stat info from the original file
+        clearstatcache(false, $file);
+        $stat = stat($file);
+        $this->writeFileHeader(
+            $name,
+            $stat[4],
+            $stat[5],
+            fileperms($file),
+            filesize($file),
+            filemtime($file),
+            false
+        );
+
+        while(!feof($fp)) {
+            $packed = pack("a512", fread($fp, 512));
+            $this->writebytes($packed);
+        }
+        fclose($fp);
+    }
+
+    /**
+     * Add a file to the current TAR archive using in memory data
+     *
+     * @param     $name
+     * @param     $data
+     * @param int $uid
+     * @param int $gid
+     * @param int $perm
+     * @param int $mtime
+     * @throws TarIOException
+     * @throws TarBadFilename
+     */
+    public function addData($name, $data, $uid = 0, $gid = 0, $perm = 0666, $mtime = 0) {
+        if($this->closed) throw(new TarIOException('Archive has been closed, files can no longer be added'));
+
+        $name = $this->cleanPath($name);
+
+        // FIXME ustar should support up 256 chars
+        if(strlen($name) > 99) throw(new TarBadFilename('Filenames may not exceed 99 bytes: '.$name));
+
+        $len = strlen($data);
+
+        $this->writeFileHeader(
+            $name,
+            $uid,
+            $gid,
+            $perm,
+            $len,
+            ($mtime) ? $mtime : time(),
+            false
+        );
+
+        for($s = 0; $s < $len; $s += 512) {
+            $this->writebytes(pack("a512", substr($data, $s, 512)));
+        }
+    }
+
+    /**
+     * Add the closing footer to the archive if in write mode, close all file handles
+     *
+     * After a call to this function no more data can be added to the archive, for
+     * read access no reading is allowed anymore
+     *
+     * "Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which
+     * consists of two 512 blocks of zero bytes"
+     *
+     * @link http://www.gnu.org/software/tar/manual/html_chapter/tar_8.html#SEC134
+     */
+    public function close() {
+        if($this->closed) return; // we did this already
+
+        // write footer
+        if($this->writeaccess){
+            $this->writebytes(pack("a512", ""));
+            $this->writebytes(pack("a512", ""));
+        }
+
+        // close file handles
+        if($this->file){
+            if($this->comptype === Tar::COMPRESS_GZIP){
+                gzclose($this->fh);
+            }elseif($this->comptype === Tar::COMPRESS_BZIP){
+                bzclose($this->fh);
+            }else{
+                fclose($this->fh);
+            }
+
+            $this->file = '';
+            $this->fh = 0;
+        }
+
+        $this->closed = true;
+    }
+
+    /**
+     * Returns the created in-memory archive data
+     *
+     * This implicitly calls close() on the Archive
+     */
+    public function getArchive($comptype = Tar::COMPRESS_AUTO, $complevel = 9) {
+        $this->close();
+
+        if($comptype === Tar::COMPRESS_AUTO) $comptype = $this->comptype;
+        $this->compressioncheck($comptype);
+
+        if($comptype === Tar::COMPRESS_GZIP) return gzcompress($this->memory, $complevel);
+        if($comptype === Tar::COMPRESS_BZIP) return bzcompress($this->memory);
+        return $this->memory;
+    }
+
+    /**
+     * Save the created in-memory archive data
+     *
+     * Note: It more memory effective to specify the filename in the create() function and
+     * let the library work on the new file directly.
+     *
+     * @param     $file
+     * @param int $comptype
+     * @param int $complevel
+     * @throws TarIOException
+     */
+    public function save($file, $comptype = Tar::COMPRESS_AUTO, $complevel = 9) {
+        if($comptype === Tar::COMPRESS_AUTO) $comptype = $this->filetype($file);
+
+        if(!file_put_contents($file, $this->getArchive($comptype, $complevel))) {
+            throw(new TarIOException('Could not write to file: '.$file));
+        }
+    }
+
+    /**
+     * Read from the open file pointer
+     *
+     * @param int $length bytes to read
+     * @return string
+     */
+    protected function readbytes($length) {
+        if($this->comptype === Tar::COMPRESS_GZIP) {
+            return @gzread($this->fh, $length);
+        } elseif($this->comptype === Tar::COMPRESS_BZIP) {
+            return @bzread($this->fh, $length);
+        } else {
+            return @fread($this->fh, $length);
+        }
+    }
+
+    /**
+     * Write to the open filepointer or memory
+     *
+     * @param string $data
+     * @throws TarIOException
+     * @return int number of bytes written
+     */
+    protected function writebytes($data) {
+        if(!$this->file) {
+            $this->memory .= $data;
+            $written = strlen($data);
+        } elseif($this->comptype === Tar::COMPRESS_GZIP) {
+            $written = @gzwrite($this->fh, $data);
+        } elseif($this->comptype === Tar::COMPRESS_BZIP) {
+            $written = @bzwrite($this->fh, $data);
+        } else {
+            $written = @fwrite($this->fh, $data);
+        }
+        if($written === false) throw(new TarIOException('Failed to write to archive stream'));
+        return $written;
+    }
+
+    /**
+     * Skip forward in the open file pointer
+     *
+     * This is basically a wrapper around seek() (and a workarounf for bzip2)
+     *
+     * @param int  $bytes seek to this position
+     */
+    function skipbytes($bytes) {
+        if($this->comptype === Tar::COMPRESS_GZIP){
+            @gzseek($this->fh, $bytes, SEEK_CUR);
+        }elseif($this->comptype === Tar::COMPRESS_BZIP){
+            // there is no seek in bzip2, we simply read on
+            @bzread($this->fh, $bytes);
+        }else{
+            @fseek($this->fh, $bytes, SEEK_CUR);
+        }
+    }
+
+    /**
+     * Write a file header
+     *
+     * @param string $name
+     * @param int    $uid
+     * @param int    $gid
+     * @param int    $perm
+     * @param int    $size
+     * @param int    $mtime
+     * @param bool   $isdir
+     */
+    protected function writeFileHeader($name, $uid, $gid, $perm, $size, $mtime, $isdir = false) {
+        // values are needed in octal
+        $uid   = sprintf("%6s ", DecOct($uid));
+        $gid   = sprintf("%6s ", DecOct($gid));
+        $perm  = sprintf("%6s ", DecOct($perm));
+        $size  = sprintf("%11s ", DecOct($size));
+        $mtime = sprintf("%11s", DecOct($mtime));
+        $dir   = ($isdir) ? '5' : '';
+
+        $data_first = pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime);
+        $data_last  = pack("a1a100a6a2a32a32a8a8a155a12", $dir, '', '', '', '', '', '', '', '', "");
+
+        for($i = 0, $chks = 0; $i < 148; $i++)
+            $chks += ord($data_first[$i]);
+
+        for($i = 156, $chks += 256, $j = 0; $i < 512; $i++, $j++)
+            $chks += ord($data_last[$j]);
+
+        $this->writebytes($data_first);
+
+        $chks = pack("a8", sprintf("%6s ", DecOct($chks)));
+        $this->writebytes($chks.$data_last);
+    }
+
+    /**
+     * Decode the given tar file header
+     *
+     * @todo how to handle filenames >100 chars?
+     * @param string $block a 512 byte block containign the header data
+     * @return array|bool
+     */
+    protected function parseHeader($block) {
+        if(!$block || strlen($block) != 512) return false;
+
+        for($i = 0, $chks = 0; $i < 148; $i++)
+            $chks += ord($block[$i]);
+
+        for($i = 156, $chks += 256; $i < 512; $i++)
+            $chks += ord($block[$i]);
+
+        $headers = @unpack("a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor", $block);
+        if(!$headers) return false;
+
+        $return['checksum'] = OctDec(trim($headers['checksum']));
+        if($return['checksum'] != $chks) return false;
+
+        $return['filename'] = trim($headers['filename']);
+        $return['perm']     = OctDec(trim($headers['perm']));
+        $return['uid']      = OctDec(trim($headers['uid']));
+        $return['gid']      = OctDec(trim($headers['gid']));
+        $return['size']     = OctDec(trim($headers['size']));
+        $return['mtime']    = OctDec(trim($headers['mtime']));
+        $return['typeflag'] = $headers['typeflag'];
+        $return['link']     = trim($headers['link']);
+        $return['uname']    = trim($headers['uname']);
+        $return['gname']    = trim($headers['gname']);
+
+        return $return;
+    }
+
+    /**
+     * Cleans up a path and removes relative parts
+     *
+     * @param string $p_dir
+     * @return string
+     */
+    protected function cleanPath($p_dir) {
+        $r = '';
+        if($p_dir) {
+            $subf = explode("/", $p_dir);
+
+            for($i = count($subf) - 1; $i >= 0; $i--) {
+                if($subf[$i] == ".") {
+                    # do nothing
+                } elseif($subf[$i] == "..") {
+                    $i--;
+                } elseif(!$subf[$i] && $i != count($subf) - 1 && $i) {
+                    # do nothing
+                } else {
+                    $r = $subf[$i].($i != (count($subf) - 1) ? "/".$r : "");
+                }
+            }
+        }
+        return $r;
+    }
+
+    /**
+     * Checks if the given compression type is available and throws an exception if not
+     *
+     * @param $comptype
+     * @throws TarIllegalCompressionException
+     */
+    protected function compressioncheck($comptype) {
+        if($comptype === Tar::COMPRESS_GZIP && !function_exists('gzopen')) {
+            throw(new TarIllegalCompressionException('No gzip support available'));
+        }
+
+        if($comptype === Tar::COMPRESS_BZIP && !function_exists('bzopen')) {
+            throw(new TarIllegalCompressionException('No bzip2 support available'));
+        }
+    }
+
+    /**
+     * Guesses the wanted compression from the given filename extension
+     *
+     * You don't need to call this yourself. It's used when you pass Tar::COMPRESS_AUTO somewhere
+     *
+     * @param string $file
+     * @return int
+     */
+    public function filetype($file) {
+        $file = strtolower($file);
+        if(substr($file, -3) == '.gz' || substr($file, -4) == '.tgz') {
+            $comptype = Tar::COMPRESS_GZIP;
+        } elseif(substr($file, -4) == '.bz2' || substr($file, -4) == '.tbz') {
+            $comptype = Tar::COMPRESS_BZIP;
+        } else {
+            $comptype = Tar::COMPRESS_NONE;
+        }
+        return $comptype;
+    }
+}
+
+class TarBadFilename extends Exception {
+}
+
+class TarIOException extends Exception {
+}
+
+class TarIllegalCompressionException extends Exception {
+}
\ No newline at end of file
diff --git a/inc/load.php b/inc/load.php
index b8a279523..49c307054 100644
--- a/inc/load.php
+++ b/inc/load.php
@@ -71,6 +71,7 @@ function load_autoload($name){
         'IXR_IntrospectionServer' => DOKU_INC.'inc/IXR_Library.php',
         'Doku_Plugin_Controller'=> DOKU_INC.'inc/plugincontroller.class.php',
         'GeSHi'                 => DOKU_INC.'inc/geshi.php',
+        'Tar'                   => DOKU_INC.'inc/Tar.class.php',
         'TarLib'                => DOKU_INC.'inc/TarLib.class.php',
         'ZipLib'                => DOKU_INC.'inc/ZipLib.class.php',
         'DokuWikiFeedCreator'   => DOKU_INC.'inc/feedcreator.class.php',
-- 
GitLab