From bee9f377bc547c99fe99b4e38199cb92cf668554 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Sat, 3 Nov 2012 17:54:02 +0100 Subject: 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 --- inc/Tar.class.php | 614 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 inc/Tar.class.php (limited to 'inc/Tar.class.php') 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 @@ +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 + * @author Bouchon (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 -- cgit v1.2.3 From 421a2704022cbc8fa07ab673c2d503199f460b8e Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Sun, 4 Nov 2012 10:36:58 +0100 Subject: Tar: Added extraction support for long file names Supports POSIX ustar prefixes and GNU longlink entries --- inc/Tar.class.php | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) (limited to 'inc/Tar.class.php') diff --git a/inc/Tar.class.php b/inc/Tar.class.php index 7f5e5af4a..8da30e736 100644 --- a/inc/Tar.class.php +++ b/inc/Tar.class.php @@ -204,7 +204,7 @@ class Tar { // is this a file? if(!$header['typeflag']){ - $fp = @fopen($output, "wb"); + $fp = fopen($output, "wb"); if(!$fp) throw(new TarIOException('Could not open file for writing: '.$output)); $size = floor($header['size'] / 512); @@ -508,7 +508,6 @@ class Tar { /** * 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 */ @@ -521,22 +520,36 @@ class Tar { 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; + $header = @unpack("a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix", $block); + if(!$header) return false; - $return['checksum'] = OctDec(trim($headers['checksum'])); + $return['checksum'] = OctDec(trim($header['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['filename'] = trim($header['filename']); + $return['perm'] = OctDec(trim($header['perm'])); + $return['uid'] = OctDec(trim($header['uid'])); + $return['gid'] = OctDec(trim($header['gid'])); + $return['size'] = OctDec(trim($header['size'])); + $return['mtime'] = OctDec(trim($header['mtime'])); + $return['typeflag'] = $header['typeflag']; + $return['link'] = trim($header['link']); + $return['uname'] = trim($header['uname']); + $return['gname'] = trim($header['gname']); + + // Handle ustar Posix compliant path prefixes + if(trim($header['prefix'])) $return['filename'] = trim($header['prefix']).'/'.$return['filename']; + + // Handle Long-Link entries from GNU Tar + if($return['typeflag'] == 'L'){ + // following data block(s) is the filename + $filename = trim($this->readbytes(ceil($header['size'] / 512) * 512)); + // next block is the real header + $block = $this->readbytes(512); + $return = $this->parseHeader($block); + // overwrite the filename + $return['filename'] = $filename; + } return $return; } -- cgit v1.2.3 From 90a1db709d3590e849a5a4966fbdf8fb58ae75cd Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Sun, 4 Nov 2012 11:31:20 +0100 Subject: Tar: support for creating archives with long filenames The library now creates either a POSIX ustar prefix or a GNU longlink entry for files which have a name longer than 100 bytes --- inc/Tar.class.php | 121 +++++++++++++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 57 deletions(-) (limited to 'inc/Tar.class.php') diff --git a/inc/Tar.class.php b/inc/Tar.class.php index 8da30e736..59e14c705 100644 --- a/inc/Tar.class.php +++ b/inc/Tar.class.php @@ -4,6 +4,8 @@ * To keep things simple, the modification of existing archives is not supported. It handles * uncompressed, gzip and bzip2 compressed tar files. * + * Long pathnames (>100 chars) are supported in POSIX ustar and GNU longlink formats. + * * To list the contents of an existing TAR archive, open() it and use contents() on it: * * $tar = new Tar(); @@ -145,10 +147,10 @@ class Tar { * @throws TarIOException * @return array */ - function extract($outdir, $strip='', $exclude='', $include='') { + function extract($outdir, $strip = '', $exclude = '', $include = '') { if($this->closed || !$this->file) throw(new TarIOException('Can not read from a closed archive')); - $outdir = rtrim($outdir,'/'); + $outdir = rtrim($outdir, '/'); io_mkdir_p($outdir); $striplen = strlen($strip); @@ -164,32 +166,32 @@ class Tar { $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']){ + $parts = explode('/', $filename); + if(!$header['typeflag']) { $base = array_pop($parts); // keep filename itself - }else{ + } else { $base = ''; } - $filename = join('/',array_slice($parts,$strip)); + $filename = join('/', array_slice($parts, $strip)); if($base) $filename .= "/$base"; - }else{ + } else { // ifstrip is a string, we strip a prefix here - if(substr($filename,0,$striplen) == $strip) $filename = substr($filename,$striplen); + if(substr($filename, 0, $striplen) == $strip) $filename = substr($filename, $striplen); } // check if this should be extracted $extract = true; - if(!$filename){ + if(!$filename) { $extract = false; - }else{ - if($include){ - if(preg_match($include, $filename)){ + } else { + if($include) { + if(preg_match($include, $filename)) { $extract = true; - }else{ + } else { $extract = false; } } - if($exclude && preg_match($exclude, $filename)){ + if($exclude && preg_match($exclude, $filename)) { $extract = false; } } @@ -203,7 +205,7 @@ class Tar { io_mkdir_p($directory); // is this a file? - if(!$header['typeflag']){ + if(!$header['typeflag']) { $fp = fopen($output, "wb"); if(!$fp) throw(new TarIOException('Could not open file for writing: '.$output)); @@ -216,10 +218,10 @@ class Tar { fclose($fp); touch($output, $header['mtime']); chmod($output, $header['perm']); - }else{ + } else { $this->skipbytes(ceil($header['size'] / 512) * 512); // the size is usually 0 for directories } - }else{ + } else { $this->skipbytes(ceil($header['size'] / 512) * 512); } } @@ -261,7 +263,7 @@ class Tar { if(!$this->fh) throw(new TarIOException('Could not open file for writing: '.$this->file)); } $this->writeaccess = false; - $this->closed = false; + $this->closed = false; } /** @@ -270,7 +272,6 @@ class Tar { * @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 = '') { @@ -279,9 +280,6 @@ class Tar { 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)); @@ -294,8 +292,7 @@ class Tar { $stat[5], fileperms($file), filesize($file), - filemtime($file), - false + filemtime($file) ); while(!feof($fp)) { @@ -306,26 +303,21 @@ class Tar { } /** - * Add a file to the current TAR archive using in memory data + * Add a file to the current TAR archive using the given $data as content * - * @param $name - * @param $data - * @param int $uid - * @param int $gid - * @param int $perm - * @param int $mtime + * @param string $name + * @param string $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); + $len = strlen($data); $this->writeFileHeader( $name, @@ -333,8 +325,7 @@ class Tar { $gid, $perm, $len, - ($mtime) ? $mtime : time(), - false + ($mtime) ? $mtime : time() ); for($s = 0; $s < $len; $s += 512) { @@ -357,23 +348,23 @@ class Tar { if($this->closed) return; // we did this already // write footer - if($this->writeaccess){ + if($this->writeaccess) { $this->writebytes(pack("a512", "")); $this->writebytes(pack("a512", "")); } // close file handles - if($this->file){ - if($this->comptype === Tar::COMPRESS_GZIP){ + if($this->file) { + if($this->comptype === Tar::COMPRESS_GZIP) { gzclose($this->fh); - }elseif($this->comptype === Tar::COMPRESS_BZIP){ + } elseif($this->comptype === Tar::COMPRESS_BZIP) { bzclose($this->fh); - }else{ + } else { fclose($this->fh); } $this->file = ''; - $this->fh = 0; + $this->fh = 0; } $this->closed = true; @@ -460,12 +451,12 @@ class Tar { * @param int $bytes seek to this position */ function skipbytes($bytes) { - if($this->comptype === Tar::COMPRESS_GZIP){ + if($this->comptype === Tar::COMPRESS_GZIP) { @gzseek($this->fh, $bytes, SEEK_CUR); - }elseif($this->comptype === Tar::COMPRESS_BZIP){ + } elseif($this->comptype === Tar::COMPRESS_BZIP) { // there is no seek in bzip2, we simply read on @bzread($this->fh, $bytes); - }else{ + } else { @fseek($this->fh, $bytes, SEEK_CUR); } } @@ -479,19 +470,38 @@ class Tar { * @param int $perm * @param int $size * @param int $mtime - * @param bool $isdir + * @param string $typeflag Set to '5' for directories */ - protected function writeFileHeader($name, $uid, $gid, $perm, $size, $mtime, $isdir = false) { + protected function writeFileHeader($name, $uid, $gid, $perm, $size, $mtime, $typeflag = '') { + // handle filename length restrictions + $prefix = ''; + $namelen = strlen($name); + if($namelen > 100) { + $file = basename($name); + $dir = dirname($name); + if(strlen($file) > 100 || strlen($dir) > 155) { + // we're still too large, let's use GNU longlink + $this->writeFileHeader('././@LongLink', 0, 0, 0, $namelen, 0, 'L'); + for($s = 0; $s < $namelen; $s += 512) { + $this->writebytes(pack("a512", substr($name, $s, 512))); + } + $name = substr($name, 0, 100); // cut off name + } else { + // we're fine when splitting, use POSIX ustar + $prefix = $dir; + $name = $file; + } + } + // 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, '', '', '', '', '', '', '', '', ""); + $data_last = pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, ""); for($i = 0, $chks = 0; $i < 148; $i++) $chks += ord($data_first[$i]); @@ -541,11 +551,11 @@ class Tar { if(trim($header['prefix'])) $return['filename'] = trim($header['prefix']).'/'.$return['filename']; // Handle Long-Link entries from GNU Tar - if($return['typeflag'] == 'L'){ + if($return['typeflag'] == 'L') { // following data block(s) is the filename $filename = trim($this->readbytes(ceil($header['size'] / 512) * 512)); // next block is the real header - $block = $this->readbytes(512); + $block = $this->readbytes(512); $return = $this->parseHeader($block); // overwrite the filename $return['filename'] = $filename; @@ -617,9 +627,6 @@ class Tar { } } -class TarBadFilename extends Exception { -} - class TarIOException extends Exception { } -- cgit v1.2.3 From f99ea547962bcbfb89921395f8aa82be59699303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Wed, 23 Jan 2013 13:45:47 +0200 Subject: fixes from bee9f377bc547c99fe99b4e38199cb92cf668554 commit notes --- inc/Tar.class.php | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) (limited to 'inc/Tar.class.php') diff --git a/inc/Tar.class.php b/inc/Tar.class.php index 59e14c705..20f397395 100644 --- a/inc/Tar.class.php +++ b/inc/Tar.class.php @@ -17,7 +17,7 @@ * * $tar = new Tar(); * $tar->open('myfile.tgz'); - * $tar->extract(/tmp); + * $tar->extract('/tmp'); * * To create a new TAR archive directly on the filesystem (low memory requirements), create() it, * add*() files and close() it: @@ -81,7 +81,7 @@ class Tar { $this->fh = @fopen($this->file, 'rb'); } - if(!$this->fh) throw(new TarIOException('Could not open file for reading: '.$this->file)); + if(!$this->fh) throw new TarIOException('Could not open file for reading: '.$this->file); $this->closed = false; } @@ -107,9 +107,9 @@ class Tar { * 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')); + if($this->closed || !$this->file) throw new TarIOException('Can not read from a closed archive'); - $result = Array(); + $result = array(); while($read = $this->readbytes(512)) { $header = $this->parseHeader($read); if(!is_array($header)) continue; @@ -148,7 +148,7 @@ class Tar { * @return array */ function extract($outdir, $strip = '', $exclude = '', $include = '') { - if($this->closed || !$this->file) throw(new TarIOException('Can not read from a closed archive')); + if($this->closed || !$this->file) throw new TarIOException('Can not read from a closed archive'); $outdir = rtrim($outdir, '/'); io_mkdir_p($outdir); @@ -207,7 +207,7 @@ class Tar { // is this a file? if(!$header['typeflag']) { $fp = fopen($output, "wb"); - if(!$fp) throw(new TarIOException('Could not open file for writing: '.$output)); + if(!$fp) throw new TarIOException('Could not open file for writing: '.$output); $size = floor($header['size'] / 512); for($i = 0; $i < $size; $i++) { @@ -260,7 +260,7 @@ class Tar { $this->fh = @fopen($this->file, 'wb'); } - if(!$this->fh) throw(new TarIOException('Could not open file for writing: '.$this->file)); + if(!$this->fh) throw new TarIOException('Could not open file for writing: '.$this->file); } $this->writeaccess = false; $this->closed = false; @@ -275,13 +275,13 @@ class Tar { * @throws TarIOException */ public function addFile($file, $name = '') { - if($this->closed) throw(new TarIOException('Archive has been closed, files can no longer be added')); + if($this->closed) throw new TarIOException('Archive has been closed, files can no longer be added'); if(!$name) $name = $file; $name = $this->cleanPath($name); $fp = fopen($file, 'rb'); - if(!$fp) throw(new TarIOException('Could not open file for reading: '.$file)); + 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); @@ -314,7 +314,7 @@ class Tar { * @throws TarIOException */ 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')); + if($this->closed) throw new TarIOException('Archive has been closed, files can no longer be added'); $name = $this->cleanPath($name); $len = strlen($data); @@ -401,7 +401,7 @@ class Tar { 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)); + throw new TarIOException('Could not write to file: '.$file); } } @@ -439,14 +439,14 @@ class Tar { } else { $written = @fwrite($this->fh, $data); } - if($written === false) throw(new TarIOException('Failed to write to archive stream')); + 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) + * This is basically a wrapper around seek() (and a workaround for bzip2) * * @param int $bytes seek to this position */ @@ -494,11 +494,11 @@ class Tar { } // 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)); + $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)); $data_first = pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime); $data_last = pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, ""); @@ -511,7 +511,7 @@ class Tar { $this->writebytes($data_first); - $chks = pack("a8", sprintf("%6s ", DecOct($chks))); + $chks = pack("a8", sprintf("%6s ", decoct($chks))); $this->writebytes($chks.$data_last); } @@ -598,11 +598,11 @@ class Tar { */ protected function compressioncheck($comptype) { if($comptype === Tar::COMPRESS_GZIP && !function_exists('gzopen')) { - throw(new TarIllegalCompressionException('No gzip support available')); + throw new TarIllegalCompressionException('No gzip support available'); } if($comptype === Tar::COMPRESS_BZIP && !function_exists('bzopen')) { - throw(new TarIllegalCompressionException('No bzip2 support available')); + throw new TarIllegalCompressionException('No bzip2 support available'); } } @@ -631,4 +631,4 @@ class TarIOException extends Exception { } class TarIllegalCompressionException extends Exception { -} \ No newline at end of file +} -- cgit v1.2.3 From f2cb3ec76dec3fe2b40f25765ef842223c7132fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Wed, 23 Jan 2013 13:55:25 +0200 Subject: handle bzip1 as well in fact .tbz is tar.bz (bzip1) and .tbz2 is what tar.bz2 is used commonly. --- inc/Tar.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'inc/Tar.class.php') diff --git a/inc/Tar.class.php b/inc/Tar.class.php index 59e14c705..86fe65abb 100644 --- a/inc/Tar.class.php +++ b/inc/Tar.class.php @@ -618,7 +618,7 @@ class Tar { $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') { + } elseif(substr($file, -4) == '.bz2' || substr($file, -3) == '.bz' || substr($file, -4) == '.tbz' || substr($file, -5) == ".tbz2") { $comptype = Tar::COMPRESS_BZIP; } else { $comptype = Tar::COMPRESS_NONE; @@ -631,4 +631,4 @@ class TarIOException extends Exception { } class TarIllegalCompressionException extends Exception { -} \ No newline at end of file +} -- cgit v1.2.3 From 7afccd0aab62eed3797273e5241b7729bab1fb3f Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Wed, 23 Jan 2013 16:16:18 +0100 Subject: Revert "handle bzip1 as well" This reverts commit f2cb3ec76dec3fe2b40f25765ef842223c7132fe. Turns out I was too fast merging this. I can't get PHP's bzip handler to handle a bzip1 compressed file. --- inc/Tar.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'inc/Tar.class.php') diff --git a/inc/Tar.class.php b/inc/Tar.class.php index 20d892e68..20f397395 100644 --- a/inc/Tar.class.php +++ b/inc/Tar.class.php @@ -618,7 +618,7 @@ class Tar { $file = strtolower($file); if(substr($file, -3) == '.gz' || substr($file, -4) == '.tgz') { $comptype = Tar::COMPRESS_GZIP; - } elseif(substr($file, -4) == '.bz2' || substr($file, -3) == '.bz' || substr($file, -4) == '.tbz' || substr($file, -5) == ".tbz2") { + } elseif(substr($file, -4) == '.bz2' || substr($file, -4) == '.tbz') { $comptype = Tar::COMPRESS_BZIP; } else { $comptype = Tar::COMPRESS_NONE; -- cgit v1.2.3