<?php
/*
Author: aldo
For: SnowCMS (www.snowcms.com) or for whatever use
License: GNU GPL v3 License (www.gnu.org/licenses/gpl-3.0.txt )
Class: Tar
With this class, you can open and read tarballs and gzipped tarballs.
You can also create tarballs, and gzip them as well.
*/
class Tar
{
# Variable: filename
private $filename;
# Variable: filemtime
private $filemtime;
# Variable: fp
private $fp;
# Variable: mode
private $mode;
# Variable: files
private $files;
# Variable: gzipped
private $is_gzipped;
# Variable: is_ustar
private $is_ustar;
/*
Constructor: __construct
*/
public function __construct($filename = null, $mode = 'r')
{
$this->filename = null;
$this->filemtime = null;
$this->fp = null;
$this->mode = null;
$this->files = null;
$this->is_gzipped = null;
$this->is_ustar = null;
if(!empty($filename))
$this->open($filename, $mode);
}
/*
Method: open
Opens the specified tar file for either reading or writing.
Parameters:
string $filename - The name of the file to open.
string $mode - r to open the tar file for reading, and w
to open the tar file for writing.
Returns:
bool - Returns true on success, false on failure.
Note:
If the mode is set to reading, opening if the file doesn't exist,
of course. However, if set to writing, the file will be created if
it doesn't exist already, but if it does, the file will be overwritten!
*/
public function open($filename, $mode = 'r')
{
# Already doing something right now? Sorry!
if(!empty($this->mode))
return false;
$mode = strtolower($mode);
if($mode == 'r')
{
# Open the file for reading, if it exists!
if(!file_exists($filename) || (file_exists($filename) && !is_file($filename)))
return false;
$filename = realpath($filename);
# Now open it for reading.
$fp = fopen($filename, 'rb');
if(empty($fp))
return false;
$this->filename = $filename;
$this->fp = $fp;
$this->mode = 'r';
# It's mine now ;) Sorta.
flock($this->fp, LOCK_SH);
# Check to see if it is gzipped, because if it is, that needs handling first!
# The first couple of bytes will tell us that...
$magic = unpack('H2a/H2b', fread($this->fp, 2));
if(strtolower($magic['a']. $magic['b']) == '1f8b')
$this->is_gzipped = true;
else
$this->is_gzipped = false;
# Not gzipped? We can check if it is UStar formatted!
$this->check_format();
# Back to 0!
fseek($this->fp, 0);
return true;
}
elseif($mode == 'w')
{
# Just try to open it.
$fp = fopen($filename, 'wb');
if(empty($fp))
return false;
$this->filename = $filename;
$this->fp = $fp;
$this->mode = 'w';
$this->files = array();
$this->is_gzipped = false;
# This time it IS mine :P
flock($this->fp, LOCK_EX);
return true;
}
return false;
}
/*
Method: check_format
Checks to see if the current tarball is setup with the older format, or the
newer UStar format.
Parameters:
none
Returns:
void - Nothing is returned by this method.
*/
private function check_format()
{
if(empty($this->mode) || $this->mode != 'r' || $this->is_gzipped())
return;
fseek($this->fp, 257);
# At position 257, there should be ustar...
$ustar = strtolower(trim(str_replace(chr(0), '', fread($this->fp, 6))));
$this->is_ustar = $ustar == 'ustar';
fseek($this->fp, 0);
}
/*
Method: files
When the mode is read, then all the file information inside the current
tar file will be returned, otherwise the current files which will be added
to the tar file will be returned.
Parameters:
none
Returns:
array
*/
public function files()
{
if(empty($this->mode) || ($this->mode == 'r' && $this->is_gzipped()))
return false;
if($this->mode == 'r')
{
# Did we already do this? Make sure the file hasn't been modified since, either.
if($this->filemtime !== null && $this->filemtime >= filemtime($this->filename) && !empty($this->files))
return $this->files;
# Get the number of bytes in the file.
fseek($this->fp, 0, SEEK_END);
$bytes = ftell($this->fp);
fseek($this->fp, 0);
# Some of the header data needs to be converted from octal to decimal.
$octal = array('mode', 'uid', 'gid', 'size', 'mtime', 'chksum', 'type');
# And then the format of what we shall read!
$format = 'a100name/a8mode/a8uid/a8gid/a12size/a12mtime/a8chksum/a1type/a100linkname';
# Now, if the tar is in UStar format, we read a bit extra ;)
if($this->is_ustar())
$format .= '/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix';
$files = array();
while($bytes > 0)
{
$file = unpack($format, ($header = fread($this->fp, 512)));
# Remove extra spacing, and convert octals to decimals!
foreach($file as $key => $value)
{
$file[$key] = trim($value);
if(in_array($key, $octal))
$file[$key] = octdec($file[$key]);
}
# Is it a file or directory?
$file['is_dir'] = substr($file['name'], -1, 1) == '/';
# Just save the position of the file, for later use!
$file['pos'] = ftell($this->fp);
# Ignore the file data, for now (The file size must be a multiple of 512)...
$seek = $file['size'] + (($file['size'] / (double)512) == 0 ? 0 : 512 - ($file['size'] % 512));
fseek($this->fp, $seek, SEEK_CUR);
# Remove some bytes.
$bytes -= 512 + $seek;
# File name not empty? Then it's good (For some reason, there are empty records added, at least 2!)
if(!empty($file['name']))
$files[] = $file;
}
# Cache it, for a bit.
$this->files = $files;
$this->filemtime = filemtime($this->filename);
return $files;
}
elseif($this->mode == 'w')
{
# Just return what we got!
return $this->files;
}
return false;
}
/*
Method: extract
Extracts the files out of the tar file, if the mode is reading.
Parameters:
string $destination - Where to extract the tarball.
bool $safe_mode - It is possible for people to have such file names as ../../someImportantFile.sys
and overwrite important system files. By setting this option to true, any ../ will
be removed from the file or directory name.
Returns:
bool - Returns true on success, false on failure.
Note:
Of course, if the file is gzipped, this method will return false, you can check out the
<Tar::ungzip> method to ungzip the tarball.
*/
public function extract($destination, $safe_mode = true)
{
if(empty($this->mode) || $this->mode != 'r' || $this->is_gzipped())
return false;
# Does the destination exist? Is it a directory?
if(!file_exists($destination))
{
$made = mkdir($destination);
if(empty($made))
# We tried, but it failed! :(
return false;
}
elseif(file_exists($destination) && !is_dir($destination))
# It isn't a directory, silly pants!
return false;
# Turn it into an absolute path.
$destination = realpath($destination);
# The files method saves the position of the file, so yeah... Simple enough, really.
$this->files();
# Before we get head over heels, are there even any files?
if(count($this->files))
{
foreach($this->files as $file)
{
# Prepend the the destination to the file name!
$file['name'] = $destination. '/'. $file['name'];
# Safe mode on..?
if(!empty($safe_mode))
$file['name'] = strtr($file['name'], array('../' => '', '/..' => ''));
# Now, is it a directory, or a file?
if($file['is_dir'])
# Make that directory, and we are done.
@mkdir($file['name']);
else
{
# It's a file, super fun!
fseek($this->fp, $file['pos']);
# Open the file that needs creation.
$fp = fopen($file['name'], 'wb');
if(empty($fp))
continue;
# Small enough to do it quickly?
if($file['size'] <= 8192 && $file['size'] > 0)
fwrite($fp, fread($this->fp, $file['size']));
elseif($file['size'] > 8192)
{
# Nope...
$left = $file['size'];
while($left > 0)
{
fwrite($fp, fread($this->fp, $left >= 8192 ? 8192 : $left));
$left -= $left >= 8192 ? 8192 : $left;
}
}
}
}
fseek($this->fp, 0);
}
return true;
}
/*
Method: ungzip
Just incase, if the tarball is gzipped, you can ungzip it via this method.
Parameters:
none
Returns:
bool - Returns true on success, false on failure.
Note:
This requires the PHP extension www.php.net/zlib, though most hosts seem to have it.
The contents of the ungzipped file will be written to the current file.
*/
public function ungzip()
{
if(empty($this->mode) || $this->mode != 'r' || !$this->is_gzipped())
return false;
# We already checked the magic number (which is how the gzipped attribute was set to true),
# next check the compression method, which should be 8! Get out the flag while we are at it too.
fseek($this->fp, 2);
$info = unpack('Ccm/Cflg', fread($this->fp, 2));
# Compression method not 8? Then I'm not up to the task! (The only method right now is 8 anyways >.>)
if($info['cm'] != 8)
return false;
# Skip past the modified time, XFL and OS, I don't really care about that :P
fseek($this->fp, 6, SEEK_CUR);
# File name? Don't want it! But gotta get past it.
if($info['flg'] & 8 || $info['flg'] & 3) # Should be 3, 7z seems to do 8? o.O
while(fread($this->fp, 1) != chr(0))
# Just keep going!
continue;
# Comment, perhaps?
if($info['flg'] & 4)
while(fread($this->fp, 1) != chr(0))
# Just keep going, again!
continue;
# CRC16 stuff?
if($info['flg'] & 1)
# Skip past the next 2 bytes.
fseek($this->fp, 2, SEEK_CUR);
# Now we need to store the data to inflate it!
$tar = '';
# How many bytes do we need to read?
$cur_pos = ftell($this->fp);
fseek($this->fp, 0, SEEK_END);
$bytes = ftell($this->fp);
fseek($this->fp, $cur_pos);
while($bytes > 0)
{
$tar .= fread($this->fp, $bytes >= 8192 ? 8192 : $bytes);
$bytes -= $bytes >= 8192 ? 8192 : $bytes;
}
# Ungzip it now, then we can write it to the current file!!!
$tar = gzinflate($tar);
# Can't write to a file that is opened in read only, can we?
fclose($this->fp);
$this->fp = fopen($this->filename, 'wb');
flock($this->fp, LOCK_EX);
fwrite($this->fp, $tar);
fclose($this->fp);
# Now open it in read only mode :P
$this->fp = fopen($this->filename, 'rb');
flock($this->fp, LOCK_SH);
# All done! And it is no longer gzipped! :)
$this->is_gzipped = false;
# Oh! And check to see the tarballs format, just incase!
$this->check_format();
return true;
}
/*
Method: add_file
Adds a file to the tarball that is currently being created.
Parameters:
string $filename - The name of the file to add to the tarball.
string $new_filename - The new name of the file (including the relative
path, so just the files name to have it in
the root directory of the tarball).
Returns:
bool - Returns true on success, false on failure.
Note:
If no new file name is supplied, and the file is within the current
working directory, then the new file name will be created automatically,
so the parameter is not required, however, if it is not within the
current working directory, adding will fail unless you supply the name.
Also, any ../ references in the new file name will be removed!
*/
public function add_file($filename, $new_filename = null)
{
# Check the usual, and whether or not the file exists.
if(empty($this->mode) || $this->mode != 'w' || !file_exists($filename) || !is_file($filename))
return false;
# Resolve the absolute file path.
$filename = realpath($filename);
# No new filename supplied? Alright, I'll try my best!
if(empty($new_filename) && substr($filename, 0, strlen(getcwd())) == getcwd())
$new_filename = substr($filename, strlen(getcwd()) + 1, strlen($filename));
elseif(empty($new_filename))
return false;
# Is the new file name a directory? Nuh uh!
if(substr($new_filename, -1, 1) == '/')
return false;
# Remove any ./ or ../
$new_filename = strtr($new_filename, array('../' => '', '/..' => ''));
# Add it to the files array, and thats it, for now.
$this->files[$new_filename] = array(
'name' => $filename,
'stat' => stat($filename),
);
return true;
}
/*
Method: add_from_string
Adds a file from a string to the tarball that is currently being created.
Parameters:
string $filename - The name of the file that will be created inside the tarball.
string $file - The contents of the file.
Returns:
bool - Returns true on success, false on failure.
Note:
Just as with <Tar::add_file>, any ../ references in the file name will
be removed.
*/
public function add_from_string($filename, $file)
{
# Check the usual and whether or not you are trying to make a directory ;)
if(empty($this->mode) || $this->mode != 'w' || empty($filename) || substr($filename, -1, 1) == '/')
return false;
# No ../ ;)
$filename = strtr($filename, array('../' => '', '/..' => ''));
# Simply add the file data.
$this->files[$filename] = array(
'data' => $file,
'stat' => array(
'dev' => 0,
'ino' => 0,
'mode' => 755,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => strlen($file),
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0,
),
);
return true;
}
/*
Method: add_empty_dir
Adds an empty directory to the tarball that is currently being created.
Parameters:
string $dirname - The directory name to be created inside the tarball.
Returns:
bool - Returns true on success, false on failure.
Note:
Any ../ references will be removed.
*/
public function add_empty_dir($dirname)
{
if(empty($this->mode) || $this->mode != 'w' || empty($dirname))
return false;
# Don't have a / at the end, it is needed, but I can do it :P
if(substr($dirname, -1, 1) != '/')
$dirname .= '/';
$dirname = strtr($dirname, array('../' => '', '/..' => ''));
# Add it, done!
$this->files[$dirname] = array(
'stat' => array(
'dev' => 0,
'ino' => 0,
'mode' => 755,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => 0,
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0,
),
);
return true;
}
/*
Method: set_gzip
When the tarball is created (written to the file), and if this is set to true,
and tarball will be gzipped before the file is closed.
Parameters:
bool $gzip - Whether or not to gzip the tarball.
Returns:
bool - Returns true on success, false on failure.
*/
public function set_gzip($gzip = true)
{
if(empty($this->mode) || $this->mode != 'w')
return false;
$this->is_gzipped = !empty($gzip);
return true;
}
/*
Method: save
Saves the created tarball into a file.
Parameters:
none
Returns:
bool - Returns true on success, false on failure.
Note:
If the file is written successfully, the <Tar::close> method is called automatically.
Also, if you want to have the tarball gzipped or in the UStar format, check out
the <Tar::set_gzip> and <Tar::set_ustar> methods.
*/
public function save()
{
if(empty($this->mode) || $this->mode != 'w')
return false;
fseek($this->fp, 0);
if(count($this->files) > 0)
{
# Used later :P
$format = array(
'mode' => array(6, ' '. chr(0)),
'uid' => array(6, ' '. chr(0)),
'gid' => array(6, ' '. chr(0)),
'size' => array(11, ' '),
'mtime' => array(11, ' '),
);
foreach($this->files as $filename => $file)
{
# Some special stuff needs to be done to certain things ;)
foreach($format as $key => $f)
{
$file['stat'][$key] = str_pad(decoct($file['stat'][$key]), $f[0], ' ', STR_PAD_LEFT). $f[1];
}
# Make the generic header...
$header = str_pad($filename, 100, chr(0)). $file['stat']['mode']. $file['stat']['uid']. $file['stat']['gid']. $file['stat']['size']. $file['stat']['mtime']. ' '. (!isset($file['data']) && !isset($file['name']) ? 5 : 0). str_repeat(chr(0), 100);
# Calculate the headers checksum by converting it to their decimal value...
$checksum = 0;
for($i = 0; $i < 257; $i++)
$checksum += ord($header[$i]);
$checksum = decoct($checksum);
# Make it again, but with extra padding ;)
$header = str_pad($filename, 100, chr(0)). $file['stat']['mode']. $file['stat']['uid']. $file['stat']['gid']. $file['stat']['size']. $file['stat']['mtime']. str_pad($checksum, 6, ' ', STR_PAD_LEFT). ' '. chr(0). (!isset($file['data']) && !isset($file['name']) ? 5 : 0). str_repeat(chr(0), 355);
# Write the header to the file now.
fwrite($this->fp, $header);
# Now for the file data...
$data = isset($file['data']) ? $file['data'] : (isset($file['name']) ? file_get_contents($file['name']) : '');
# We may need to append NUL bytes in order to make it take up multiples of 512 bytes.
$length = octdec($file['stat']['size']);
if($length > 0 && ($length / (double)512) != 0)
$data .= str_repeat(chr(0), 512 - ($length % 512));
# And the data!
fwrite($this->fp, $data);
}
}
# The end of the tar contains at least 2 512 byte blocks of NUL's... Weird.
fwrite($this->fp, str_repeat(chr(0), 1024));
# Did you want this to be gzipped..?
if($this->is_gzipped())
{
# Save the file name, close everything, then get the files contents.
$filename = $this->filename;
$this->close();
$file = file_get_contents($filename);
# Open up the file, in writing mode, of course!
$fp = fopen($filename, 'wb');
# MINE!
flock($fp, LOCK_EX);
# Just one second! If you already added a .gz to the end of the file, let's just remove it
# for the sake of the "original" name ;)
if(substr($filename, -3, 3) == '.gz')
$filename = substr($filename, 0, strlen($filename) - 3);
# Now write it :P
fwrite($fp, chr(31). chr(139). chr(8). chr(8). pack('V', filemtime($filename)). chr(0). chr(0). basename($filename). chr(0). gzdeflate($file, 9). pack('VV', crc32($file), strlen($file)));
fclose($fp);
}
$this->close();
return true;
}
/*
Method: close
Closes all opened files and sets all attributes to null.
Parameters:
none
Returns:
void - Nothing is returned by this method.
Note:
This method is automatically called in the objects destructor.
*/
public function close()
{
if(!empty($this->mode))
{
@fclose($this->fp);
$this->filename = null;
$this->filemtime = null;
$this->fp = null;
$this->mode = null;
$this->files = null;
$this->is_gzipped = null;
$this->is_ustar = null;
}
}
/*
Method: filename
Parameters:
none
Returns:
string - Returns the current file which has been opened with <Tar::open>.
*/
public function filename()
{
return $this->filename;
}
/*
Method: mode
Parameters:
none
Returns:
string - Returns the current mode, r for read, w for write, null for nothing.
*/
public function mode()
{
return $this->mode;
}
/*
Method: is_gzipped
Parameters:
none
Returns:
bool - Returns true if the current tarball is gzipped, false if not.
Note:
The file does not need to be opened in read only in order for this to return true,
if you set the file to be gzipped when creating a tarball, this will return true
as well.
*/
public function is_gzipped()
{
return $this->is_gzipped;
}
/*
Method: is_ustar
Parameters:
none
Returns:
bool - Returns true if the current tarball is in the UStar format, false if not.
*/
public function is_ustar()
{
return $this->is_ustar;
}
/*
Destructor: __destruct
*/
public function __destruct()
{
$this->close();
}
}
?>