You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

769 lines
20 KiB

<?php
/**
* Abstract base class for all archive file inspectors.
*
* @author Hecks
* @copyright (c) 2010-2013 Hecks
* @license Modified BSD
* @version 3.1
*/
abstract class ArchiveReader
{
// ------ Class variables and methods -----------------------------------------
/**
* Unpacks data from a binary string.
*
* This method helps in particular to fix unpacking of unsigned longs on 32-bit
* systems due to PHP internal quirks.
*
* @param string $format format codes for unpacking
* @param string $data the packed string
* @param boolean $fixLongs should unsigned longs be fixed?
* @return array the unpacked data
*/
public static function unpack($format, $data, $fixLongs=true)
{
$unpacked = unpack($format, $data);
$longs = 'VNL';
// Fix conversion of unsigned longs on 32-bit systems
if ($fixLongs && PHP_INT_SIZE <= 4 && preg_match("/[{$longs}]++/", $format)) {
$codes = explode('/', $format);
foreach ($unpacked as $key => $value) {
$code = array_shift($codes);
if (strpos($longs, $code[0]) !== false && $value < 0) {
$unpacked[$key] = $value + 0x100000000; // converts to float
}
}
}
return $unpacked;
}
/**
* Converts two longs to a float to represent a 64-bit integer on 32-bit
* systems, otherwise returns the integer.
*
* If more precision is needed, the bcmath functions should be used.
*
* @param integer $low the low 32 bits
* @param integer $high the high 32 bits
* @return float|integer
*/
public static function int64($low, $high)
{
return ($low + ($high * 0x100000000));
}
/**
* Converts DOS standard timestamps to UNIX timestamps.
*
* @param integer $dostime DOS timestamp
* @return integer UNIX timestamp
*/
public static function dos2unixtime($dostime)
{
$sec = 2 * ($dostime & 0x1f);
$min = ($dostime >> 5) & 0x3f;
$hrs = ($dostime >> 11) & 0x1f;
$day = ($dostime >> 16) & 0x1f;
$mon = ($dostime >> 21) & 0x0f;
$year = (($dostime >> 25) & 0x7f) + 1980;
return mktime($hrs, $min, $sec, $mon, $day, $year);
}
/**
* Converts Windows FILETIME format timestamps to UNIX timestamps.
*
* @param integer $low the low 32 bits
* @param integer $high the high 32 bits
* @return integer UNIX timestamp
*/
public static function win2unixtime($low, $high)
{
$ushift = 116444736000000000;
$ftime = self::int64($low, $high);
return (int) floor(($ftime - $ushift) / 10000000);
}
/**
* Converts a numeric value passed by reference to a hexadecimal string.
*
* @param mixed $value the numeric value to convert
* @return void
*/
public static function convert2hex(&$value)
{
if (is_numeric($value)) {
$value = base_convert($value, 10, 16);
}
}
/**
* Calculates the size of the given file.
*
* This is fiddly on 32-bit systems for sizes larger than 2GB due to internal
* limitations - filesize() returns a signed long - and so needs hackery.
*
* @param string $file full path to the file
* @return integer|float the file size in bytes
*/
public static function getFileSize($file)
{
// 64-bit systems should be OK
if (PHP_INT_SIZE > 4)
return filesize($file);
// Hack for Windows
if (DIRECTORY_SEPARATOR === '\\') {
if (! extension_loaded('com_dotnet')) {
return trim(shell_exec('for %f in ('.escapeshellarg($file).') do @echo %~zf')) + 0;
}
$com = new COM('Scripting.FileSystemObject');
$f = $com->GetFile($file);
return $f->Size + 0;
}
// Hack for *nix
$os = php_uname();
if (stripos($os, 'Darwin') !== false) {
$command = 'stat -f %z '.escapeshellarg($file);
} elseif (stripos($os, 'DiskStation') !== false) {
$command = 'ls -l '.escapeshellarg($file).' | awk \'{print $5}\'';
} else {
$command = 'stat -c %s '.escapeshellarg($file);
}
return trim(shell_exec($command)) + 0;
}
/**
* Returns human-readable byte sizes as formatted strings.
*
* @param integer $bytes the size to format
* @param integer $round decimal places limit
* @return string human-readable size
*/
public static function formatSize($bytes, $round=1)
{
$suffix = array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
for ($i = 0; $bytes > 1024 && isset($suffix[$i+1]); $i++) {$bytes /= 1024;}
return round($bytes, $round).' '.$suffix[$i];
}
/**
* Creates a directory if it doesn't already exist.
*
* @param string $dir the directory path
* @return boolean false if the directory already exists
*/
public static function makeDirectory($dir)
{
if (file_exists($dir))
return false;
mkdir($dir, 0777, TRUE);
chmod($dir, 0777);
return true;
}
/**
* Returns all the positions of a case-sensitive needle in a haystack string.
* With an array of needles, the result will be a sorted list with the positions
* as keys and a list of all matching needle keys as the values.
*
* @param string $haystack the string to search
* @param string|array $needle the string or list of strings to find
* @return array|boolean the needle positions, or false if none found
*/
public static function strposall($haystack, $needle, $offset=0)
{
$start = $offset;
$hlen = strlen($haystack);
$isArray = is_array($needle);
$results = array();
foreach ((array) $needle as $key => $value) {
if (($vlen = strlen($value)) == 0)
continue;
while ($offset < $hlen && ($pos = strpos($haystack, $value, $offset)) !== false) {
$offset = $pos + $vlen;
if ($isArray) {
$results[$pos][] = $key;
} else {
$results[$pos] = $pos;
}
}
$offset = $start;
}
if (!empty($results)) {
ksort($results);
return $isArray ? $results : array_values($results);
}
return false;
}
// ------ Instance variables and methods ---------------------------------------
/**
* The last error message.
* @var string
*/
public $error = '';
/**
* The number of files in the archive file/data.
* @var integer
*/
public $fileCount = 0;
/**
* Default constructor for loading and analyzing archive files.
*
* @param string $file path to the archive file
* @param boolean $isFragment true if file is an archive fragment
* @param array $range the start and end byte positions
* @return void
*/
public function __construct($file=null, $isFragment=false, array $range=null)
{
if ($file) $this->open($file, $isFragment, $range);
}
/**
* Opens a handle to the archive file and analyzes the archive contents,
* optionally within a defined byte range only.
*
* @param string $file path to the file
* @param boolean $isFragment true if file is an archive fragment
* @param array $range the start and end byte positions
* @return boolean false if archive analysis fails
*/
public function open($file, $isFragment=false, array $range=null)
{
$this->reset();
$this->isFragment = $isFragment;
if (!$this->setRange($range)) {return false;}
if (!$file || !($archive = realpath($file)) || !is_file($archive)) {
$this->error = "File does not exist ($file)";
return false;
}
$this->file = $archive;
$this->fileSize = self::getFileSize($archive);
if (!$this->end) {$this->end = $this->fileSize - 1;}
if (!$this->checkRange()) {return false;}
// Open the file handle
$this->handle = fopen($archive, 'rb');
$this->rewind();
return $this->analyze();
}
/**
* Loads data up to maxReadBytes and analyzes the archive contents, optionally
* within a defined byte range only.
*
* This method is recommended when dealing with file fragments.
*
* @param string $data archive data to be analyzed
* @param boolean $isFragment true if data is an archive fragment
* @param array $range the start and end byte positions
* @return boolean false if archive analysis fails
*/
public function setData($data, $isFragment=false, array $range=null)
{
$this->reset();
$this->isFragment = $isFragment;
if (!$this->setRange($range)) {return false;}
if (($dsize = strlen($data)) == 0) {
$this->error = 'No data was passed, nothing to analyze';
return false;
}
// Store the data locally up to max bytes
$data = ($dsize > $this->maxReadBytes) ? substr($data, 0, $this->maxReadBytes) : $data;
$this->dataSize = strlen($data);
if (!$this->end) {$this->end = $this->dataSize - 1;}
if (!$this->checkRange()) {return false;}
$this->data = $data;
$this->rewind();
return $this->analyze();
}
/**
* Closes any open file handle and unsets any stored data.
*
* @return void
*/
public function close()
{
if (is_resource($this->handle)) {
fclose($this->handle);
$this->handle = null;
}
$this->data = '';
}
/**
* Sets the maximum number of stored data bytes to analyze.
*
* @param integer $bytes the max bytes to read
* @return void
*/
public function setMaxReadBytes($bytes)
{
if (is_int($bytes) && $bytes > 0) {
$this->maxReadBytes = $bytes;
}
}
/**
* A full summary will be returned by default when converting the archive
* object to a string, such as when echoing it.
*
* @return string archive summary
*/
public function __toString()
{
return print_r($this->getSummary(true), true);
}
/**
* Magic method for accessing protected properties.
*
* @param string $name the property name
* @return mixed the property value
* @throws RuntimeException
* @throws LogicException
*/
public function __get($name)
{
// For backwards compatibility
if ($name == 'file') {return $this->file;}
if (!isset($this->$name))
throw new RuntimeException('Undefined property: '.get_class($this).'::$'.$name);
throw new LogicException('Cannot access protected property '.get_class($this).'::$'.$name);
}
/**
* Class destructor.
*
* @return void
*/
public function __destruct()
{
$this->deleteTempFiles();
}
/**
* Convenience method that outputs a summary list of the archive information,
* useful for pretty-printing.
*
* @param boolean $full return a full summary?
* @return array archive summary
*/
abstract public function getSummary($full=false);
/**
* Parses the stored archive info and returns a list of records for each of the
* files in the archive.
*
* @return array list of file records, empty if none are available
*/
abstract public function getFileList();
/**
* Returns the position of the archive marker/signature.
*
* @return mixed Marker position, or false if none found
*/
abstract public function findMarker();
/**
* Parses the archive data and stores the results locally.
*
* @return boolean false if parsing fails
*/
abstract protected function analyze();
/**
* Path to the archive file (if any).
* @var string
*/
protected $file = '';
/**
* File handle for the current archive.
* @var resource
*/
protected $handle;
/**
* The maximum number of stored data bytes to analyze.
* @var integer
*/
protected $maxReadBytes = 1048576;
/**
* The maximum length of filenames (for sanity checking).
* @var integer
*/
protected $maxFilenameLength = 256;
/**
* Is this a file/data fragment?
* @var boolean
*/
protected $isFragment = false;
/**
* The stored archive file data.
* @var string
*/
protected $data = '';
/**
* The size in bytes of the currently stored data.
* @var integer
*/
protected $dataSize = 0;
/**
* The size in bytes of the archive file.
* @var integer
*/
protected $fileSize = 0;
/**
* The starting position for the analysis.
* @var integer
*/
protected $start = 0;
/**
* The ending position for the analysis.
* @var integer
*/
protected $end = 0;
/**
* The number of bytes to analyze from the $start position.
* @var integer
*/
protected $length = 0;
/**
* The current position relative to the $start position.
* @var integer
*/
protected $offset = 0;
/**
* The position of the marker/signature relative to the $start position.
* @var integer
*/
protected $markerPosition;
/**
* The list of any temporary files created by the reader.
* @var array
*/
protected $tempFiles = array();
/**
* Sets the absolute start and end positions in the file/data to be analyzed
* (zero-indexed and inclusive of the end byte).
*
* @param array $range the start and end byte positions
* @return boolean false if ranges are invalid
*/
protected function setRange(array $range=null)
{
$start = isset($range[0]) ? (int) $range[0] : 0;
$end = isset($range[1]) ? (int) $range[1] : 0;
if ($start != $range[0] || $end != $range[1] || $start < 0 || $end < 0) {
$this->error = "Start ($start) and end ($end) points must be positive integers";
return false;
}
if ($end < $start) {
$this->error = "End point ($end) must be higher than start point ($start)";
return false;
}
$this->start = $start;
$this->end = $end;
return $this->checkRange();
}
/**
* Determines whether the currently set start and end ranges are within the
* bounds of the available data, and if not sets an error message.
*
* @return boolean
*/
protected function checkRange()
{
$this->length = $this->end - $this->start + 1;
$mlen = $this->file ? $this->fileSize : $this->dataSize;
if ($mlen && ($this->end >= $mlen || $this->start >= $mlen || $this->length < 1)) {
$this->error = "Byte range ({$this->start}-{$this->end}) is invalid";
return false;
}
$this->error = '';
return true;
}
/**
* Returns data within the given absolute byte range of the current file/data.
*
* @param array $range the absolute start and end positions
* @return string|boolean the requested data or false on error
*/
protected function getRange(array $range)
{
// Check that the requested range is valid
$original = array($this->start, $this->end, $this->length);
if (!$this->setRange($range)) {
list($this->start, $this->end, $this->length) = $original;
return false;
}
// Get the data
$this->seek(0);
$data = $this->read($this->length);
// Restore the original range
list($this->start, $this->end, $this->length) = $original;
return $data;
}
/**
* Saves data within the given absolute byte range of the current file/data to
* the destination file.
*
* @param array $range the absolute start and end positions
* @param string $destination full path of the file to create
* @return integer|boolean number of bytes written or false on error
*/
protected function saveRange(array $range, $destination)
{
// Check that the requested range is valid
$original = array($this->start, $this->end, $this->length);
if (!$this->setRange($range)) {
list($this->start, $this->end, $this->length) = $original;
return false;
}
// Write the buffered data to disk
$this->seek(0);
$fh = fopen($destination, 'wb');
$rlen = $this->length;
$written = 0;
while ($this->offset < $this->length) {
$data = $this->read(min(1024, $rlen));
$rlen -= strlen($data);
$written += fwrite($fh, $data);
}
fclose($fh);
// Restore the original range
list($this->start, $this->end, $this->length) = $original;
return $written;
}
/**
* Reads the given number of bytes from the archive file/data and moves the
* offset pointer forward.
*
* @param integer $num number of bytes to read
* @return string the byte string
* @throws InvalidArgumentException
* @throws RangeException
*/
protected function read($num)
{
if ($num == 0) return '';
// Check that enough data is available
$newPos = $this->offset + $num;
if ($num < 1 || $newPos > $this->length)
throw new InvalidArgumentException("Could not read {$num} bytes from offset {$this->offset}");
// Read the requested bytes
if ($this->file && is_resource($this->handle)) {
$read = fread($this->handle, $num);
} elseif ($this->data) {
$read = substr($this->data, $this->tell(), $num);
}
// Confirm the read length
if (!isset($read) || (($rlen = strlen($read)) < $num)) {
$rlen = isset($rlen) ? $rlen : 'none';
$this->error = "Not enough data to read ({$num} bytes requested, {$rlen} available)";
throw new RangeException($this->error);
}
// Move the data pointer
$this->offset = $newPos;
return $read;
}
/**
* Moves the current offset pointer to a position in the stored data or file
* relative to the start position.
*
* Note that seeking in files past the 2GB limit on 32-bit systems is either
* impossible or needs an incredibly slow hack due to the fseek() pointer not
* behaving after 2GB. The only real solution here is to use a 64-bit system.
*
* @param integer $pos new pointer position
* @return void
* @throws RuntimeException
* @throws InvalidArgumentException
*/
protected function seek($pos)
{
if ($pos > $this->length || $pos < 0)
throw new InvalidArgumentException("Could not seek to {$pos} (max: {$this->length})");
if ($this->file && is_resource($this->handle)) {
$max = PHP_INT_MAX;
$file_pos = $this->start + $pos;
if ($file_pos >= $max) {
$this->error = 'The file is too large for this PHP version (> '.self::formatSize($max).')';
throw new RuntimeException($this->error);
}
fseek($this->handle, $file_pos, SEEK_SET);
}
$this->offset = $pos;
}
/**
* Provides the absolute position within the current file/data rather than
* the offset relative to the defined start position.
*
* @return integer the absolute file/data position
*/
protected function tell()
{
if ($this->file && is_resource($this->handle))
return ftell($this->handle);
return $this->start + $this->offset;
}
/**
* Sets the file/data offset pointer to the starting position.
*
* @return void
*/
protected function rewind()
{
if ($this->file && is_resource($this->handle)) {
rewind($this->handle);
}
$this->seek(0);
}
/**
* Saves the current stored data to a temporary file and returns its name.
*
* @return string path to the temporary file
*/
protected function createTempDataFile()
{
list($hash, $dest) = $this->getTempFileName();
if (file_exists($dest))
return $this->tempFiles[$hash] = $dest;
file_put_contents($dest, $this->data);
chmod($dest, 0777);
return $this->tempFiles[$hash] = $dest;
}
/**
* Calculates a temporary file name based on hashes of the given data string
* or the stored data.
*
* @param string $data the source data to be hashed
* @return array the hash and temporary file path values
*/
protected function getTempFileName($data=null)
{
$hash = $data ? md5($data) : md5(substr($this->data, 0, 16*1024));
$path = $this->getTempDirectory().DIRECTORY_SEPARATOR.$hash.'.tmp';
return array($hash, $path);
}
/**
* Returns the absolute path to the directory for storing temporary files,
* and creates any parent directories if they don't already exist.
*
* @return string the temporary directory path
*/
protected function getTempDirectory()
{
$dir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'archivereader';
self::makeDirectory($dir);
return $dir;
}
/**
* Deletes any temporary files created by the reader.
*
* @return void
*/
protected function deleteTempFiles()
{
foreach ($this->tempFiles as $temp) {
@unlink($temp);
}
$this->tempFiles = array();
}
/**
* Resets the instance variables before parsing new data.
*
* @return void
*/
protected function reset()
{
$this->close();
$this->file = '';
$this->data = '';
$this->fileSize = 0;
$this->dataSize = 0;
$this->start = 0;
$this->end = 0;
$this->length = 0;
$this->offset = 0;
$this->error = '';
$this->isFragment = false;
$this->fileCount = 0;
$this->markerPosition = null;
$this->deleteTempFiles();
clearstatcache();
}
} // End ArchiveReader class