mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-09-01 18:58:56 +00:00
1579 lines
44 KiB
PHP
1579 lines
44 KiB
PHP
<?php
|
|
|
|
require_once dirname(__FILE__) . '/archivereader.php';
|
|
require_once dirname(__FILE__) . '/pipereader.php';
|
|
|
|
/**
|
|
* RarInfo class.
|
|
*
|
|
* A simple class for inspecting RAR file data and listing information about
|
|
* the archive contents in pure PHP (no external dependencies). Data can be
|
|
* streamed from a file or loaded directly from memory.
|
|
*
|
|
* Example usage:
|
|
*
|
|
* <code>
|
|
*
|
|
* // Load the RAR file or data
|
|
* $rar = new RarInfo;
|
|
* $rar->open('./foo.rar'); // or $rar->setData($data);
|
|
* if ($rar->error) {
|
|
* echo "Error: {$rar->error}\n";
|
|
* exit;
|
|
* }
|
|
*
|
|
* // Check encryption
|
|
* if ($rar->isEncrypted) {
|
|
* echo "Archive is password encrypted\n";
|
|
* exit;
|
|
* }
|
|
*
|
|
* // Process the file list
|
|
* $files = $rar->getFileList();
|
|
* foreach ($files as $file) {
|
|
* if ($file['pass'] == true) {
|
|
* echo "File is passworded: {$file['name']}\n";
|
|
* }
|
|
* if ($file['compressed'] == false) {
|
|
* echo "Extracting uncompressed file: {$file['name']}\n";
|
|
* $rar->saveFileData($file['name'], "./destination/{$file['name']}");
|
|
* // or $data = $rar->getFileData($file['name']);
|
|
* }
|
|
* }
|
|
*
|
|
* </code>
|
|
*
|
|
* For RAR file fragments - i.e. that may not contain a valid Marker Block - add
|
|
* TRUE as the second parameter for the open() or setData() methods to skip the
|
|
* error messages and allow a forced search for valid File Header blocks.
|
|
*
|
|
* @author Hecks
|
|
* @copyright (c) 2010-2014 Hecks
|
|
* @license Modified BSD
|
|
* @version 5.6
|
|
*/
|
|
class RarInfo extends ArchiveReader
|
|
{
|
|
// ------ Class constants -----------------------------------------------------
|
|
|
|
/**#@+
|
|
* RAR 1.5 - 4.x archive format values (thanks to Marko Kreen)
|
|
*/
|
|
|
|
// Block types
|
|
const BLOCK_MARK = 0x72;
|
|
const BLOCK_MAIN = 0x73;
|
|
const BLOCK_FILE = 0x74;
|
|
const BLOCK_OLD_COMMENT = 0x75;
|
|
const BLOCK_OLD_EXTRA = 0x76;
|
|
const BLOCK_OLD_SUB = 0x77;
|
|
const BLOCK_OLD_RECOVERY = 0x78;
|
|
const BLOCK_OLD_AUTH = 0x79;
|
|
const BLOCK_SUB = 0x7a;
|
|
const BLOCK_ENDARC = 0x7b;
|
|
const BLOCK_NULL = 0x00;
|
|
|
|
// Flags for BLOCK_MAIN
|
|
const MAIN_VOLUME = 0x0001;
|
|
const MAIN_COMMENT = 0x0002;
|
|
const MAIN_LOCK = 0x0004;
|
|
const MAIN_SOLID = 0x0008;
|
|
const MAIN_NEWNUMBERING = 0x0010;
|
|
const MAIN_AUTH = 0x0020;
|
|
const MAIN_RECOVERY = 0x0040;
|
|
const MAIN_PASSWORD = 0x0080;
|
|
const MAIN_FIRSTVOLUME = 0x0100;
|
|
const MAIN_ENCRYPTVER = 0x0200;
|
|
|
|
// Flags for BLOCK_FILE
|
|
const FILE_SPLIT_BEFORE = 0x0001;
|
|
const FILE_SPLIT_AFTER = 0x0002;
|
|
const FILE_PASSWORD = 0x0004;
|
|
const FILE_COMMENT = 0x0008;
|
|
const FILE_SOLID = 0x0010;
|
|
const FILE_DICTMASK = 0x00e0;
|
|
const FILE_DICT64 = 0x0000;
|
|
const FILE_DICT128 = 0x0020;
|
|
const FILE_DICT256 = 0x0040;
|
|
const FILE_DICT512 = 0x0060;
|
|
const FILE_DICT1024 = 0x0080;
|
|
const FILE_DICT2048 = 0x00a0;
|
|
const FILE_DICT4096 = 0x00c0;
|
|
const FILE_DIRECTORY = 0x00e0;
|
|
const FILE_LARGE = 0x0100;
|
|
const FILE_UNICODE = 0x0200;
|
|
const FILE_SALT = 0x0400;
|
|
const FILE_VERSION = 0x0800;
|
|
const FILE_EXTTIME = 0x1000;
|
|
const FILE_EXTFLAGS = 0x2000;
|
|
|
|
// Flags for BLOCK_ENDARC
|
|
const ENDARC_NEXT_VOLUME = 0x0001;
|
|
const ENDARC_DATACRC = 0x0002;
|
|
const ENDARC_REVSPACE = 0x0004;
|
|
const ENDARC_VOLNR = 0x0008;
|
|
|
|
// Flags for all blocks
|
|
const SKIP_IF_UNKNOWN = 0x4000;
|
|
const LONG_BLOCK = 0x8000;
|
|
|
|
// Subtypes for BLOCK_SUB
|
|
const SUBTYPE_COMMENT = 'CMT';
|
|
const SUBTYPE_ACL = 'ACL';
|
|
const SUBTYPE_STREAM = 'STM';
|
|
const SUBTYPE_UOWNER = 'UOW';
|
|
const SUBTYPE_AUTHVER = 'AV';
|
|
const SUBTYPE_RECOVERY = 'RR';
|
|
const SUBTYPE_OS2EA = 'EA2';
|
|
const SUBTYPE_BEOSEA = 'EABE';
|
|
|
|
// Compression methods
|
|
const METHOD_STORE = 0x30;
|
|
const METHOD_FASTEST = 0x31;
|
|
const METHOD_FAST = 0x32;
|
|
const METHOD_NORMAL = 0x33;
|
|
const METHOD_GOOD = 0x34;
|
|
const METHOD_BEST = 0x35;
|
|
|
|
// OS types
|
|
const OS_MSDOS = 0;
|
|
const OS_OS2 = 1;
|
|
const OS_WIN32 = 2;
|
|
const OS_UNIX = 3;
|
|
const OS_MACOS = 4;
|
|
const OS_BEOS = 5;
|
|
|
|
/**#@-*/
|
|
|
|
/**
|
|
* Size in bytes of the main part of each block header.
|
|
*/
|
|
const HEADER_SIZE = 7;
|
|
|
|
/**
|
|
* Format for unpacking the main part of each block header.
|
|
*/
|
|
const FORMAT_BLOCK_HEADER = 'vhead_crc/Chead_type/vhead_flags/vhead_size';
|
|
|
|
/**
|
|
* Format for unpacking the remainder of a File block header.
|
|
*/
|
|
const FORMAT_FILE_HEADER = 'Vpack_size/Vunp_size/Chost_os/Vfile_crc/Vftime/Cunp_ver/Cmethod/vname_size/Vattr';
|
|
|
|
/**
|
|
* RAR archive format version.
|
|
*/
|
|
const FMT_RAR14 = '1.4';
|
|
const FMT_RAR15 = '1.5';
|
|
const FMT_RAR50 = '5.0';
|
|
|
|
/**#@+
|
|
* RAR 5.0 archive format values
|
|
*/
|
|
|
|
// Block types
|
|
const R50_BLOCK_MAIN = 0x01;
|
|
const R50_BLOCK_FILE = 0x02;
|
|
const R50_BLOCK_SERVICE = 0x03;
|
|
const R50_BLOCK_CRYPT = 0x04;
|
|
const R50_BLOCK_ENDARC = 0x05;
|
|
|
|
// Flags for all block types
|
|
const R50_HAS_EXTRA = 0x0001;
|
|
const R50_HAS_DATA = 0x0002;
|
|
const R50_SKIP_IF_UNKNOWN = 0x0004;
|
|
const R50_SPLIT_BEFORE = 0x0008;
|
|
const R50_SPLIT_AFTER = 0x0010;
|
|
const R50_IS_CHILD = 0x0020;
|
|
const R50_INHERITED = 0x0040;
|
|
|
|
// Service block types
|
|
const R50_SERVICE_COMMENT = 'CMT';
|
|
const R50_SERVICE_QUICKOPEN = 'QO';
|
|
const R50_SERVICE_ACL = 'ACL';
|
|
const R50_SERVICE_STREAM = 'STM';
|
|
const R50_SERVICE_RECOVERY = 'RR';
|
|
|
|
// Flags for R50_BLOCK_MAIN
|
|
const R50_MAIN_VOLUME = 0x0001;
|
|
const R50_MAIN_VOLNUMBER = 0x0002;
|
|
const R50_MAIN_SOLID = 0x0004;
|
|
const R50_MAIN_RECOVERY = 0x0008;
|
|
const R50_MAIN_LOCK = 0x0010;
|
|
|
|
// Flags for R50_BLOCK_FILE
|
|
const R50_FILE_DIRECTORY = 0x0001;
|
|
const R50_FILE_UTIME = 0x0002;
|
|
const R50_FILE_CRC32 = 0x0004;
|
|
const R50_FILE_UNPUNKNOWN = 0x0008;
|
|
|
|
// Flags for R50_BLOCK_ENDARC
|
|
const R50_ENDARC_NEXT_VOLUME = 0x0001;
|
|
|
|
// Extra record types for R50_BLOCK_MAIN
|
|
const R50_MEXTRA_LOCATOR = 0x01;
|
|
|
|
// Flags for R50_MEXTRA_LOCATOR
|
|
const R50_MEXTRA_LOC_QLIST = 0x0001;
|
|
const R50_MEXTRA_LOC_RR = 0x0002;
|
|
|
|
// Extra record types for R50_BLOCK_FILE
|
|
const R50_FEXTRA_CRYPT = 0x01;
|
|
const R50_FEXTRA_HASH = 0x02;
|
|
const R50_FEXTRA_HTIME = 0x03;
|
|
const R50_FEXTRA_VERSION = 0x04;
|
|
const R50_FEXTRA_REDIR = 0x05;
|
|
const R50_FEXTRA_UOWNER = 0x06;
|
|
const R50_FEXTRA_SUBDATA = 0x07;
|
|
|
|
// Flags for R50_FEXTRA_HTIME
|
|
const R50_FEXTRA_HT_UNIX = 0x0001;
|
|
const R50_FEXTRA_HT_MTIME = 0x0002;
|
|
const R50_FEXTRA_HT_CTIME = 0x0004;
|
|
const R50_FEXTRA_HT_ATIME = 0x0008;
|
|
|
|
// Compression methods
|
|
const R50_METHOD_STORE = 0;
|
|
const R50_METHOD_FASTEST = 1;
|
|
const R50_METHOD_FAST = 2;
|
|
const R50_METHOD_NORMAL = 3;
|
|
const R50_METHOD_GOOD = 4;
|
|
const R50_METHOD_BEST = 5;
|
|
|
|
// OS types
|
|
const R50_OS_WIN32 = 0;
|
|
const R50_OS_UNIX = 1;
|
|
|
|
/**#@-*/
|
|
|
|
// ------ Instance variables and methods ---------------------------------------
|
|
|
|
/**
|
|
* Signature for the RAR Marker block.
|
|
* @var string
|
|
*/
|
|
protected $markerBlock = "\x52\x61\x72\x21\x1a\x07\x00";
|
|
|
|
/**
|
|
* Signature for RAR 5.0 format archives.
|
|
* @var string
|
|
*/
|
|
protected $markerRar50 = "\x52\x61\x72\x21\x1a\x07\x01\x00";
|
|
|
|
/**
|
|
* List of block names corresponding to block types.
|
|
* @var array
|
|
*/
|
|
protected $blockNames = array(
|
|
// RAR 1.5 - 4.x
|
|
self::BLOCK_MARK => 'Marker',
|
|
self::BLOCK_MAIN => 'Archive',
|
|
self::BLOCK_FILE => 'File',
|
|
self::BLOCK_OLD_COMMENT => 'Old Style Comment',
|
|
self::BLOCK_OLD_EXTRA => 'Old Style Extra Info',
|
|
self::BLOCK_OLD_SUB => 'Old Style Subblock',
|
|
self::BLOCK_OLD_RECOVERY => 'Old Style Recovery Record',
|
|
self::BLOCK_OLD_AUTH => 'Old Style Archive Authenticity',
|
|
self::BLOCK_SUB => 'Subblock',
|
|
self::BLOCK_ENDARC => 'Archive End',
|
|
self::BLOCK_NULL => 'Null Block',
|
|
// RAR 5.0
|
|
self::R50_BLOCK_MAIN => 'Archive',
|
|
self::R50_BLOCK_FILE => 'File',
|
|
self::R50_BLOCK_SERVICE => 'Service',
|
|
self::R50_BLOCK_CRYPT => 'Archive Encryption',
|
|
self::R50_BLOCK_ENDARC => 'Archive End',
|
|
);
|
|
|
|
/**
|
|
* List of the names corresponding to Subblock types (0x7a).
|
|
* @var array
|
|
*/
|
|
protected $subblockNames = array(
|
|
self::SUBTYPE_COMMENT => 'Comment',
|
|
self::SUBTYPE_ACL => 'Access Control List',
|
|
self::SUBTYPE_STREAM => 'Stream',
|
|
self::SUBTYPE_UOWNER => 'Owner/Group Information',
|
|
self::SUBTYPE_AUTHVER => 'Authenticity Verification',
|
|
self::SUBTYPE_RECOVERY => 'Recovery Record',
|
|
self::SUBTYPE_OS2EA => 'OS2EA',
|
|
self::SUBTYPE_BEOSEA => 'BEOSEA',
|
|
);
|
|
|
|
/**
|
|
* List of the names corresponding to RAR 5.0 Service types.
|
|
* @var array
|
|
*/
|
|
protected $serviceNames = array(
|
|
self::R50_SERVICE_COMMENT => 'Archive Comment',
|
|
self::R50_SERVICE_QUICKOPEN => 'Archive Quick Open',
|
|
self::R50_SERVICE_ACL => 'Access Control List',
|
|
self::R50_SERVICE_STREAM => 'NTFS Stream',
|
|
self::R50_SERVICE_RECOVERY => 'Recovery Record',
|
|
);
|
|
|
|
/**
|
|
* Is the volume attribute set for the archive?
|
|
* @var boolean
|
|
*/
|
|
public $isVolume = false;
|
|
|
|
/**
|
|
* Is authenticity information present?
|
|
* @var boolean
|
|
*/
|
|
public $hasAuth = false;
|
|
|
|
/**
|
|
* Is a recovery record present?
|
|
* @var boolean
|
|
*/
|
|
public $hasRecovery = false;
|
|
|
|
/**
|
|
* Is the archive encrypted with a password?
|
|
* @var boolean
|
|
*/
|
|
public $isEncrypted = false;
|
|
|
|
/**
|
|
* The RAR format version for the current archive.
|
|
* @var string
|
|
*/
|
|
public $format = '';
|
|
|
|
/**
|
|
* Any RAR 5.0 archive comments.
|
|
* @var string
|
|
*/
|
|
public $comments = '';
|
|
|
|
/**
|
|
* Convenience method that outputs a summary list of the archive information,
|
|
* useful for pretty-printing.
|
|
*
|
|
* @param boolean $full add file list to output?
|
|
* @param boolean $skipDirs should directory entries be skipped?
|
|
* @return array archive summary
|
|
*/
|
|
public function getSummary($full=false, $skipDirs=false)
|
|
{
|
|
$summary = array(
|
|
'file_name' => $this->file,
|
|
'file_size' => $this->fileSize,
|
|
'data_size' => $this->dataSize,
|
|
'use_range' => "{$this->start}-{$this->end}",
|
|
'is_volume' => (int) $this->isVolume,
|
|
'has_auth' => (int) $this->hasAuth,
|
|
'has_recovery' => (int) $this->hasRecovery,
|
|
'is_encrypted' => (int) $this->isEncrypted,
|
|
'rar_format' => $this->format,
|
|
);
|
|
if ($this->comments) {
|
|
$summary['comments'] = $this->comments;
|
|
}
|
|
$fileList = $this->getFileList($skipDirs);
|
|
$summary['file_count'] = count($fileList);
|
|
if ($full) {
|
|
$summary['file_list'] = $fileList;
|
|
}
|
|
if ($this->error) {
|
|
$summary['error'] = $this->error;
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of the blocks found in the archive, optionally in human-readable
|
|
* format (for debugging purposes only).
|
|
*
|
|
* @param boolean $format should the block data be formatted?
|
|
* @param boolean $asHex should numeric values be displayed as hexadecimal?
|
|
* @return array|boolean list of blocks, or false if none available
|
|
*/
|
|
public function getBlocks($format=true, $asHex=false)
|
|
{
|
|
// Check that blocks are stored
|
|
if (empty($this->blocks)) {return false;}
|
|
if (!$format) {return $this->blocks;}
|
|
|
|
// Build a formatted block list
|
|
$ret = array();
|
|
foreach ($this->blocks as $block) {
|
|
$b = $this->formatBlock($block, $asHex);
|
|
if ($b['head_type'] == self::R50_BLOCK_SERVICE && !empty($block['file_name'])
|
|
&& $b['file_name'] == self::R50_SERVICE_QUICKOPEN
|
|
) {
|
|
// Format any cached blocks
|
|
foreach ($b['cache_data'] as &$cache) {
|
|
$cache['data'] = $this->formatBlock($cache['data'], $asHex);
|
|
}
|
|
}
|
|
$ret[] = $b;
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Parses the stored blocks and returns a list of records for each of the
|
|
* files in the archive.
|
|
*
|
|
* @param boolean $skipDirs should directory entries be skipped?
|
|
* @return array list of file records, empty if none are available
|
|
*/
|
|
public function getFileList($skipDirs=false)
|
|
{
|
|
$ret = array();
|
|
foreach ($this->blocks as $block) {
|
|
if (($block['head_type'] == self::BLOCK_FILE || $block['head_type'] == self::R50_BLOCK_FILE)
|
|
&& !empty($block['file_name'])
|
|
) {
|
|
if ($skipDirs && !empty($block['is_dir'])) {continue;}
|
|
$ret[] = $this->getFileBlockSummary($block);
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Returns a summary list of any RAR 5.0 Quick Open cached file headers.
|
|
*
|
|
* @param boolean $skipDirs should directory entries be skipped?
|
|
* @return array|boolean list of file record, or false if none available
|
|
*/
|
|
public function getQuickOpenFileList($skipDirs=false)
|
|
{
|
|
// Check that blocks are stored
|
|
if (empty($this->blocks)) {return false;}
|
|
|
|
$ret = array();
|
|
foreach ($this->blocks as $block) {
|
|
if ($block['head_type'] == self::R50_BLOCK_SERVICE && !empty($block['file_name'])
|
|
&& $block['file_name'] == self::R50_SERVICE_QUICKOPEN
|
|
) {
|
|
// Build the cached file header list
|
|
foreach ($block['cache_data'] as $cache) {
|
|
if ($cache['data']['head_type'] == self::R50_BLOCK_FILE) {
|
|
if ($skipDirs && !empty($cache['data']['is_dir'])) {continue;}
|
|
$ret[] = $this->getFileBlockSummary($cache['data'], true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the raw data for the given filename. Note that this is only useful
|
|
* if the file isn't compressed.
|
|
*
|
|
* @param string $filename name of the file to extract
|
|
* @return string|boolean file data, or false on error
|
|
*/
|
|
public function getFileData($filename)
|
|
{
|
|
// Check that blocks are stored and data source is available
|
|
if (empty($this->blocks) || ($this->data == '' && $this->handle == null))
|
|
return false;
|
|
|
|
// Get the absolute start/end positions
|
|
if (!($info = $this->getFileInfo($filename)) || empty($info['range'])) {
|
|
$this->error = "Could not find file info for: ({$filename})";
|
|
return false;
|
|
}
|
|
$this->error = '';
|
|
|
|
return $this->getRange(explode('-', $info['range']));
|
|
}
|
|
|
|
/**
|
|
* Saves the raw data for the given filename to the given destination. This
|
|
* is only useful if the file isn't compressed.
|
|
*
|
|
* @param string $filename name of the file to extract
|
|
* @param string $destination full path of the file to create
|
|
* @return integer|boolean number of bytes saved or false on error
|
|
*/
|
|
public function saveFileData($filename, $destination)
|
|
{
|
|
// Check that blocks are stored and data source is available
|
|
if (empty($this->blocks) || ($this->data == '' && $this->handle == null))
|
|
return false;
|
|
|
|
// Get the absolute start/end positions
|
|
if (!($info = $this->getFileInfo($filename)) || empty($info['range'])) {
|
|
$this->error = "Could not find file info for: ({$filename})";
|
|
return false;
|
|
}
|
|
$this->error = '';
|
|
|
|
return $this->saveRange(explode('-', $info['range']), $destination);
|
|
}
|
|
|
|
/**
|
|
* Sets the absolute path to the external Unrar client.
|
|
*
|
|
* @param string $client path to the client
|
|
* @return void
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function setExternalClient($client)
|
|
{
|
|
if ($client && (!is_file($client) || !is_executable($client)))
|
|
throw new InvalidArgumentException("Not a valid client: {$client}");
|
|
|
|
$this->externalClient = $client;
|
|
}
|
|
|
|
/**
|
|
* Extracts a compressed or encrypted file using the configured external Unrar
|
|
* client, optionally returning the data or saving it to file.
|
|
*
|
|
* @param string $filename name of the file to extract
|
|
* @param string $destination full path of the file to create
|
|
* @param string $password password to use for decryption
|
|
* @return mixed extracted data, number of bytes saved or false on error
|
|
*/
|
|
public function extractFile($filename, $destination=null, $password=null)
|
|
{
|
|
if (!$this->externalClient || (!$this->file && !$this->data)) {
|
|
$this->error = 'An external client and valid data source are needed';
|
|
return false;
|
|
}
|
|
|
|
// Check that the file is extractable
|
|
if (!($info = $this->getFileInfo($filename))) {
|
|
$this->error = "Could not find file info for: ({$filename})";
|
|
return false;
|
|
}
|
|
if (!empty($info['pass']) && $password == null) {
|
|
$this->error = "The file is passworded: ({$filename})";
|
|
return false;
|
|
}
|
|
|
|
// Set the data file source
|
|
$source = $this->file ? $this->file : $this->createTempDataFile();
|
|
|
|
// Ensure that internal file paths are valid for Mac/*nix
|
|
if (DIRECTORY_SEPARATOR !== '\\') {
|
|
$filename = str_replace('\\', '/', $filename);
|
|
}
|
|
|
|
// Set the external command
|
|
$pass = $password ? '-p'.escapeshellarg($password) : '-p-';
|
|
$command = '"'.$this->externalClient.'"'
|
|
." p -kb -y -c- -ierr -ip {$pass} -- "
|
|
.escapeshellarg($source).' '.escapeshellarg($filename);
|
|
|
|
// Set STDERR to write to a temporary file
|
|
list($hash, $errorFile) = $this->getTempFileName($source.'errors');
|
|
$this->tempFiles[$hash] = $errorFile;
|
|
$command .= ' 2> '.escapeshellarg($errorFile);
|
|
|
|
// Start the new pipe reader
|
|
$pipe = new PipeReader;
|
|
if (!$pipe->open($command)) {
|
|
$this->error = $pipe->error;
|
|
return false;
|
|
}
|
|
$this->error = '';
|
|
|
|
// Open destination file or start buffer
|
|
if ($destination) {
|
|
$handle = fopen($destination, 'wb');
|
|
$written = 0;
|
|
} else {
|
|
$data = '';
|
|
}
|
|
|
|
// Buffer the piped data or save it to file
|
|
while ($read = $pipe->read(1024, false)) {
|
|
if ($destination) {
|
|
$written += fwrite($handle, $read);
|
|
} else {
|
|
$data .= $read;
|
|
}
|
|
}
|
|
if ($destination) {fclose($handle);}
|
|
$pipe->close();
|
|
|
|
// Check for errors (only after the pipe is closed)
|
|
if (($error = @file_get_contents($errorFile)) && strpos($error, 'All OK') === false) {
|
|
if ($destination) {@unlink($destination);}
|
|
$this->error = $error;
|
|
return false;
|
|
}
|
|
|
|
return $destination ? $written : $data;
|
|
}
|
|
|
|
/**
|
|
* List of block types and Subblock subtypes without bodies.
|
|
* @var array
|
|
*/
|
|
protected $headersOnly = array(
|
|
'type' => array(),
|
|
'subtype' => array()
|
|
);
|
|
|
|
/**
|
|
* List of blocks found in the archive.
|
|
* @var array
|
|
*/
|
|
protected $blocks = array();
|
|
|
|
/**
|
|
* Full path to the external Unrar client.
|
|
* @var string
|
|
*/
|
|
protected $externalClient = '';
|
|
|
|
/**
|
|
* Returns block data in human-readable format (for debugging purposes only).
|
|
*
|
|
* @param array $block the block to format
|
|
* @param boolean $asHex should numeric values be displayed as hexadecimal?
|
|
* @return array the formatted block
|
|
*/
|
|
protected function formatBlock($block, $asHex=false)
|
|
{
|
|
$b = array();
|
|
|
|
// Add block descriptors
|
|
$b['type'] = isset($this->blockNames[$block['head_type']]) ? $this->blockNames[$block['head_type']] : 'Unknown';
|
|
if ($block['head_type'] == self::BLOCK_SUB && !empty($block['file_name'])
|
|
&& isset($this->subblockNames[$block['file_name']])
|
|
) {
|
|
$b['sub_type'] = $this->subblockNames[$block['file_name']];
|
|
}
|
|
if ($block['head_type'] == self::R50_BLOCK_SERVICE && !empty($block['file_name'])
|
|
&& isset($this->serviceNames[$block['file_name']])
|
|
) {
|
|
$b['name'] = $this->serviceNames[$block['file_name']];
|
|
}
|
|
$b += $block;
|
|
|
|
// Use hexadecimal values?
|
|
if ($asHex) {
|
|
array_walk_recursive($b, array('self', 'convert2hex'));
|
|
}
|
|
|
|
// Sanity check filename length
|
|
if (isset($b['file_name'])) {
|
|
$b['file_name'] = substr($b['file_name'], 0, $this->maxFilenameLength);
|
|
}
|
|
|
|
return $b;
|
|
}
|
|
|
|
/**
|
|
* Returns a processed summary of a RAR File block.
|
|
*
|
|
* @param array $block a valid File block
|
|
* @param array $quickOpen is this a Quick Open cached block?
|
|
* @return array summary information
|
|
*/
|
|
protected function getFileBlockSummary($block, $quickOpen=false)
|
|
{
|
|
$ret = array(
|
|
'name' => !empty($block['file_name']) ? substr($block['file_name'], 0, $this->maxFilenameLength) : 'Unknown',
|
|
'size' => isset($block['unp_size']) ? $block['unp_size'] : 0,
|
|
'date' => !empty($block['utime']) ? $block['utime'] : (!empty($block['ftime']) ? self::dos2unixtime($block['ftime']) : 0),
|
|
'pass' => isset($block['has_password']) ? ((int) $block['has_password']) : 0,
|
|
'compressed' => isset($block['method']) ? ((int) ($block['method'] != self::METHOD_STORE && $block['method'] != self::R50_METHOD_STORE)): 0,
|
|
'next_offset' => $block['next_offset'],
|
|
);
|
|
if (!empty($block['is_dir'])) {
|
|
$ret['is_dir'] = 1;
|
|
} elseif (!$quickOpen && !in_array(self::BLOCK_FILE, $this->headersOnly['type'])) {
|
|
$start = $this->start + $block['offset'] + $block['head_size'];
|
|
$end = min($this->end, $start + $block['pack_size'] - 1);
|
|
$ret['range'] = "{$start}-{$end}";
|
|
}
|
|
if (!empty($block['file_crc'])) {
|
|
$ret['crc32'] = dechex($block['file_crc']);
|
|
}
|
|
if (!empty($block['split_after']) || !empty($block['split_before'])) {
|
|
$ret['split'] = 1;
|
|
}
|
|
if (!empty($block['split_after'])) {
|
|
$ret['split_after'] = 1;
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Returns information for the given filename in the current file/data.
|
|
*
|
|
* @param string $filename the filename to search
|
|
* @return array|boolean the file info or false on error
|
|
*/
|
|
protected function getFileInfo($filename)
|
|
{
|
|
foreach ($this->getFileList(true) as $file) {
|
|
if (isset($file['name']) && $file['name'] == $filename) {
|
|
return $file;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Searches for the position of a valid File header in the file/data up to
|
|
* maxReadBytes, and sets it as the start of the data to analyze.
|
|
*
|
|
* This quite slow hack is only useful when handling RAR file fragments, and
|
|
* only with RAR format 1.5 - 4.x.
|
|
*
|
|
* @return integer|boolean the header offset, or false if none is found
|
|
*/
|
|
protected function findFileHeader()
|
|
{
|
|
// Buffer the data to search
|
|
$start = $this->offset;
|
|
try {
|
|
$buffer = $this->read(min($this->length, $this->maxReadBytes));
|
|
$this->rewind();
|
|
} catch (Exception $e) {return false;}
|
|
|
|
// Get all the offsets to test
|
|
if (!($positions = self::strposall($buffer, pack('C', self::BLOCK_FILE))))
|
|
return false;
|
|
|
|
foreach ($positions as $offset) try {
|
|
$offset += $start;
|
|
$this->seek($offset - 2);
|
|
|
|
// Run a File header CRC & sanity check
|
|
$block = $this->getNextBlock();
|
|
if ($this->checkFileHeaderCRC($block)) {
|
|
$this->seek($block['offset'] + self::HEADER_SIZE);
|
|
$this->processBlock($block);
|
|
if ($this->sanityCheckFileHeader($block)) {
|
|
|
|
// A valid File header was found
|
|
$this->format = self::FMT_RAR15;
|
|
return $this->markerPosition = $block['offset'];
|
|
}
|
|
}
|
|
|
|
// No more readable data, or read error
|
|
} catch (Exception $e) {continue;}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Runs a File Header CRC check on a valid File block.
|
|
*
|
|
* @param array $block a valid File block
|
|
* @return boolean false if CRC check fails
|
|
*/
|
|
protected function checkFileHeaderCRC($block)
|
|
{
|
|
// Not supported for RAR 5.0
|
|
if ($this->format == self::FMT_RAR50) {return false;}
|
|
|
|
try {
|
|
$this->seek($block['offset'] + 2);
|
|
$data = $this->read($block['head_size'] - 2);
|
|
$crc = crc32($data) & 0xffff;
|
|
return ($crc === $block['head_crc']);
|
|
} catch (Exception $e){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* File header CRC checks can produce false positives, so this is a
|
|
* last-ditch attempt to verify that this is actually a valid header.
|
|
*
|
|
* @param array $block the block to sanity check
|
|
* @param integer $limit the minimum failure threshold
|
|
* @return boolean false if the sanity check fails
|
|
*/
|
|
protected function sanityCheckFileHeader($block, $limit=3)
|
|
{
|
|
// Not supported for RAR 5.0
|
|
if ($this->format == self::FMT_RAR50) {return false;}
|
|
|
|
$fail = ($block['host_os'] > 5)
|
|
+ ($block['method'] > 0x35)
|
|
+ ($block['unp_ver'] > 50)
|
|
+ ($block['name_size'] > $this->maxFilenameLength)
|
|
+ ($block['pack_size'] > PHP_INT_MAX)
|
|
+ (isset($block['salt']) && !isset($block['has_password']));
|
|
|
|
return $fail < $limit;
|
|
}
|
|
|
|
/**
|
|
* Returns the position of the archive marker/signature in the stored data or file.
|
|
*
|
|
* @return mixed Marker position, or false if marker is missing
|
|
*/
|
|
public function findMarker()
|
|
{
|
|
if ($this->markerPosition !== null)
|
|
return $this->markerPosition;
|
|
|
|
try {
|
|
$buff = $this->read(min($this->length, $this->maxReadBytes));
|
|
$this->rewind();
|
|
if (($pos = strpos($buff, $this->markerBlock)) !== false) {
|
|
$this->format = self::FMT_RAR15;
|
|
} elseif (($pos = strpos($buff, $this->markerRar50)) !== false) {
|
|
$this->format = self::FMT_RAR50;
|
|
}
|
|
return $this->markerPosition = $pos;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the RAR data and stores a list of valid blocks locally.
|
|
*
|
|
* @return boolean false if parsing fails
|
|
*/
|
|
protected function analyze()
|
|
{
|
|
// Find the RAR marker, if there is one
|
|
$startPos = $this->findMarker();
|
|
if ($this->format == self::FMT_RAR50)
|
|
{
|
|
// Start after the RAR 5.0 marker signature
|
|
$this->seek($startPos + strlen($this->markerRar50));
|
|
|
|
} elseif ($startPos === false && !$this->isFragment) {
|
|
|
|
// Not a RAR fragment or valid file, so abort here
|
|
$this->error = 'Could not find Marker block, not a valid RAR file';
|
|
return false;
|
|
|
|
} elseif ($startPos !== false) {
|
|
|
|
// Start at the MARKER block
|
|
$this->seek($startPos);
|
|
|
|
} elseif ($this->isFragment) {
|
|
|
|
// Search for a valid file header and continue unpacking from there
|
|
if (($startPos = $this->findFileHeader()) === false) {
|
|
$this->error = 'Could not find a valid File header';
|
|
return false;
|
|
}
|
|
$this->seek($startPos);
|
|
}
|
|
|
|
// Analyze all valid blocks
|
|
while ($this->offset < $this->length) try {
|
|
|
|
// Get the next block header
|
|
$block = $this->getNextBlock();
|
|
|
|
// Process the current block by type
|
|
$this->processBlock($block);
|
|
|
|
// Add the current block to the list
|
|
$this->blocks[] = $block;
|
|
|
|
// Bail if this is an encrypted archive
|
|
if ($this->isEncrypted)
|
|
break;
|
|
|
|
// Skip to the next block, if any
|
|
if ($this->offset != $block['next_offset']) {
|
|
$this->seek($block['next_offset']);
|
|
}
|
|
|
|
// Sanity check
|
|
if ($block['offset'] == $this->offset) {
|
|
$this->error = 'Parsing seems to be stuck';
|
|
$this->close();
|
|
return false;
|
|
}
|
|
|
|
// No more readable data, or read error
|
|
} catch (Exception $e) {
|
|
if ($this->error) {$this->close(); return false;}
|
|
break;
|
|
}
|
|
|
|
// Check for valid blocks
|
|
if (empty($this->blocks)) {
|
|
$this->error = 'No valid RAR blocks were found';
|
|
return false;
|
|
}
|
|
|
|
// Analysis was successful
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reads the start of the next block header and returns the common block
|
|
* info before further processing by block type.
|
|
*
|
|
* @return array the next block header info
|
|
*/
|
|
protected function getNextBlock()
|
|
{
|
|
if ($this->format == self::FMT_RAR50)
|
|
return $this->getNextBlockR50();
|
|
|
|
// Start the block info
|
|
$block = array('offset' => $this->offset);
|
|
|
|
// Unpack the block header
|
|
$block += self::unpack(self::FORMAT_BLOCK_HEADER, $this->read(self::HEADER_SIZE), false);
|
|
|
|
// Check for add_size field
|
|
if (($block['head_flags'] & self::LONG_BLOCK)
|
|
&& ($block['head_type'] != self::BLOCK_FILE)
|
|
&& ($block['head_type'] != self::BLOCK_SUB)
|
|
) {
|
|
$block += self::unpack('Vadd_size', $this->read(4));
|
|
} else {
|
|
$block['add_size'] = 0;
|
|
}
|
|
|
|
// Sanity check header size
|
|
$block['head_size'] = max(self::HEADER_SIZE, $block['head_size']);
|
|
|
|
// Add offset info for next block (if any)
|
|
$block['next_offset'] = $block['offset'] + $block['head_size'] + $block['add_size'];
|
|
|
|
// Return the block info
|
|
return $block;
|
|
}
|
|
|
|
/**
|
|
* Processes a block passed by reference based on its type.
|
|
*
|
|
* @param array $block the block to process
|
|
* @return void
|
|
*/
|
|
protected function processBlock(&$block)
|
|
{
|
|
if ($this->format == self::FMT_RAR50)
|
|
return $this->processBlockR50($block);
|
|
|
|
// Block type: ARCHIVE
|
|
if ($block['head_type'] == self::BLOCK_MAIN) {
|
|
|
|
// Unpack the remainder of the Archive block header
|
|
$block += self::unpack('vreserved1/Vreserved2', $this->read(6));
|
|
|
|
// Parse Archive flags
|
|
if ($block['head_flags'] & self::MAIN_VOLUME) {
|
|
$block['is_volume'] = true;
|
|
$this->isVolume = true;
|
|
}
|
|
if ($block['head_flags'] & self::MAIN_AUTH) {
|
|
$block['has_auth'] = true;
|
|
$this->hasAuth = true;
|
|
}
|
|
if ($block['head_flags'] & self::MAIN_RECOVERY) {
|
|
$block['has_recovery'] = true;
|
|
$this->hasRecovery = true;
|
|
}
|
|
if ($block['head_flags'] & self::MAIN_PASSWORD) {
|
|
$block['is_encrypted'] = 1;
|
|
$this->isEncrypted = true;
|
|
}
|
|
}
|
|
|
|
// Block type: ARCHIVE END
|
|
elseif ($block['head_type'] == self::BLOCK_ENDARC) {
|
|
$block['more_volumes'] = (bool) ($block['head_flags'] & self::ENDARC_NEXT_VOLUME);
|
|
}
|
|
|
|
// Block type: NULL BLOCK (zero-padded)
|
|
elseif ($block['head_type'] == self::BLOCK_NULL) {
|
|
$remainder = $this->length - $block['offset'] - $block['head_size'];
|
|
if ($remainder < self::HEADER_SIZE) {
|
|
$block['next_offset'] = $this->length;
|
|
$block['add_size'] = $remainder;
|
|
}
|
|
}
|
|
|
|
// Block type: FILE or SUBBLOCK (new style)
|
|
elseif ($block['head_type'] == self::BLOCK_FILE || $block['head_type'] == self::BLOCK_SUB) {
|
|
|
|
// Unpack the remainder of the block header
|
|
$block += self::unpack(self::FORMAT_FILE_HEADER, $this->read(25));
|
|
|
|
// Large file sizes
|
|
if ($block['head_flags'] & self::FILE_LARGE) {
|
|
$block += self::unpack('Vhigh_pack_size/Vhigh_unp_size', $this->read(8));
|
|
$block['pack_size'] = self::int64($block['pack_size'], $block['high_pack_size']);
|
|
$block['unp_size'] = self::int64($block['unp_size'], $block['high_unp_size']);
|
|
}
|
|
|
|
// Is this a directory entry?
|
|
if (($block['head_flags'] & self::FILE_DICTMASK) == self::FILE_DIRECTORY) {
|
|
$block['is_dir'] = true;
|
|
}
|
|
|
|
// Filename: unicode
|
|
if ($block['head_flags'] & self::FILE_UNICODE) {
|
|
|
|
// Split the standard filename and unicode data from the file_name field
|
|
$fn = explode("\x00", $this->read($block['name_size']));
|
|
|
|
// Decompress the unicode filename, encode the result as UTF-8
|
|
$uc = new RarUnicodeFilename($fn[0], $fn[1]);
|
|
if ($ucname = $uc->decode()) {
|
|
$block['file_name'] = @iconv('UTF-16LE', 'UTF-8//IGNORE//TRANSLIT', $ucname);
|
|
|
|
// Fallback to the standard filename
|
|
} else {
|
|
$block['file_name'] = $fn[0];
|
|
}
|
|
|
|
// Filename: non-unicode
|
|
} else {
|
|
$block['file_name'] = $this->read($block['name_size']);
|
|
}
|
|
|
|
// Salt (optional)
|
|
if ($block['head_flags'] & self::FILE_SALT) {
|
|
$block['salt'] = $this->read(8);
|
|
}
|
|
|
|
// Extended time fields (optional)
|
|
if ($block['head_flags'] & self::FILE_EXTTIME) {
|
|
$block['ext_time'] = true;
|
|
}
|
|
|
|
// Encrypted with password?
|
|
if ($block['head_flags'] & self::FILE_PASSWORD) {
|
|
$block['has_password'] = true;
|
|
}
|
|
|
|
// Continued from previous volume?
|
|
if ($block['head_flags'] & self::FILE_SPLIT_BEFORE) {
|
|
$block['split_before'] = true;
|
|
}
|
|
|
|
// Continued in next volume?
|
|
if ($block['head_flags'] & self::FILE_SPLIT_AFTER) {
|
|
$block['split_after'] = true;
|
|
}
|
|
|
|
// Increment the file count
|
|
if ($block['head_type'] == self::BLOCK_FILE) {
|
|
$this->fileCount++;
|
|
}
|
|
|
|
// Update next header block offset
|
|
if (($block['head_type'] == self::BLOCK_FILE && !in_array(self::BLOCK_FILE, $this->headersOnly['type']))
|
|
|| ($block['head_type'] == self::BLOCK_SUB && !in_array($block['file_name'], $this->headersOnly['subtype']))
|
|
) {
|
|
$block['next_offset'] += $block['pack_size'];
|
|
}
|
|
}
|
|
|
|
// Parse any useful Subblock info
|
|
if ($block['head_type'] == self::BLOCK_SUB) {
|
|
|
|
// Authenticity verification
|
|
if ($block['file_name'] == self::SUBTYPE_AUTHVER) {
|
|
$block += self::unpack('vav_name_size', $this->read(2));
|
|
$block['av_file_name'] = $this->read($block['av_name_size']);
|
|
$block += self::unpack('vav_unknown/vav_cname_size', $this->read(4)); // guesswork
|
|
$block['av_created_by'] = $this->read($block['av_cname_size']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads the start of the next Rar 5.0 block header and returns the common
|
|
* block info before further processing by block type.
|
|
*
|
|
* @return array the next block header info
|
|
*/
|
|
protected function getNextBlockR50()
|
|
{
|
|
// Start the block info
|
|
$block = array('offset' => $this->offset);
|
|
|
|
// Unpack the main part of the header
|
|
$block += self::unpack('Vhead_crc', $this->read(4));
|
|
$block['head_size_r'] = $this->getVarInt();
|
|
$block['head_size'] = $this->offset - $block['offset'] + $block['head_size_r'];
|
|
$block['head_type'] = $this->getVarInt();
|
|
$block['head_flags'] = $this->getVarInt();
|
|
|
|
// Optional extra area and data area sizes
|
|
if ($block['head_flags'] & self::R50_HAS_EXTRA) {
|
|
$block['extra_size'] = $this->getVarInt();
|
|
}
|
|
if ($block['head_flags'] & self::R50_HAS_DATA) {
|
|
$block['data_size'] = $this->getVarInt();
|
|
}
|
|
|
|
// Sanity check header size
|
|
$block['head_size'] = max(self::HEADER_SIZE, $block['head_size']);
|
|
|
|
// Add offset info for next block (if any)
|
|
$block['next_offset'] = $block['offset'] + $block['head_size'];
|
|
if (isset($block['data_size'])) {
|
|
$block['next_offset'] += $block['data_size'];
|
|
}
|
|
|
|
// Return the block info
|
|
return $block;
|
|
}
|
|
|
|
/**
|
|
* Processes a RAR 5.0 block passed by reference based on its type.
|
|
*
|
|
* @param array $block the block to process
|
|
* @param array $quickOpen is this a Quick Open cached block?
|
|
* @return void
|
|
* @throws RuntimeException
|
|
*/
|
|
protected function processBlockR50(&$block, $quickOpen=false)
|
|
{
|
|
// Block type: ARCHIVE
|
|
if ($block['head_type'] == self::R50_BLOCK_MAIN) {
|
|
$block['flags'] = $this->getVarInt();
|
|
|
|
if ($block['flags'] & self::R50_MAIN_VOLUME) {
|
|
$block['is_volume'] = true;
|
|
$this->isVolume = true;
|
|
}
|
|
if ($block['flags'] & self::R50_MAIN_VOLNUMBER) {
|
|
$block['vol_number'] = $this->getVarInt();
|
|
}
|
|
if ($block['flags'] & self::R50_MAIN_RECOVERY) {
|
|
$block['has_recovery'] = true;
|
|
$this->hasRecovery = true;
|
|
}
|
|
}
|
|
|
|
// Block type: ARCHIVE END
|
|
elseif ($block['head_type'] == self::R50_BLOCK_ENDARC) {
|
|
$block['flags'] = $this->getVarInt();
|
|
$block['more_volumes'] = (bool) ($block['flags'] & self::R50_ENDARC_NEXT_VOLUME);
|
|
}
|
|
|
|
// Block type: ARCHIVE ENCRYPTION
|
|
elseif ($block['head_type'] == self::R50_BLOCK_CRYPT) {
|
|
$this->isEncrypted = true;
|
|
}
|
|
|
|
// Block type: FILE or SERVICE
|
|
elseif ($block['head_type'] == self::R50_BLOCK_FILE
|
|
|| $block['head_type'] == self::R50_BLOCK_SERVICE
|
|
) {
|
|
if (!isset($block['data_size']))
|
|
throw new RuntimeException('Required block data size is missing');
|
|
|
|
$block['flags'] = $this->getVarInt();
|
|
$block['pack_size'] = $block['data_size'];
|
|
$block['unp_size'] = $this->getVarInt();
|
|
$block['attr'] = $this->getVarInt();
|
|
$block['utime'] = 0;
|
|
|
|
if ($block['flags'] & self::R50_FILE_UTIME) {
|
|
$block += self::unpack('Vutime', $this->read(4));
|
|
}
|
|
if ($block['flags'] & self::R50_FILE_CRC32) {
|
|
$block += self::unpack('Vfile_crc', $this->read(4));
|
|
}
|
|
if ($block['flags'] & self::R50_FILE_DIRECTORY) {
|
|
$block['is_dir'] = true;
|
|
}
|
|
|
|
$block['comp_info'] = $this->getVarInt();
|
|
$block['unp_ver'] = $block['comp_info'] & 0x3f;
|
|
$block['method'] = ($block['comp_info'] >> 7) & 7;
|
|
$block['host_os'] = $this->getVarInt();
|
|
$block['name_size'] = $this->getVarInt();
|
|
$block['file_name'] = $this->read($block['name_size']);
|
|
|
|
// Increment the file count
|
|
if ($block['head_type'] == self::R50_BLOCK_FILE && !$quickOpen) {
|
|
$this->fileCount++;
|
|
}
|
|
|
|
if ($block['head_type'] == self::R50_BLOCK_SERVICE && !$quickOpen) {
|
|
|
|
// Add any archive comments
|
|
if ($block['file_name'] == self::R50_SERVICE_COMMENT) {
|
|
$this->comments = $this->read($block['pack_size']);
|
|
|
|
// Add the quick open data
|
|
} elseif ($block['file_name'] == self::R50_SERVICE_QUICKOPEN) {
|
|
$this->processQuickOpenRecords($block);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Continued from previous volume?
|
|
if ($block['head_flags'] & self::R50_SPLIT_BEFORE) {
|
|
$block['split_before'] = true;
|
|
}
|
|
|
|
// Continued in next volume?
|
|
if ($block['head_flags'] & self::R50_SPLIT_AFTER) {
|
|
$block['split_after'] = true;
|
|
}
|
|
|
|
// Process any extra records
|
|
if ($block['head_flags'] & self::R50_HAS_EXTRA) {
|
|
$this->processExtraRecords($block);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes the RAR 5.0 Quick Open block data and stores any cached headers.
|
|
*
|
|
* @param array $block the block to process
|
|
* @return void
|
|
*/
|
|
protected function processQuickOpenRecords(&$block)
|
|
{
|
|
$end = $this->offset + $block['data_size'];
|
|
while ($this->offset < $end) {
|
|
|
|
// Start the cache record
|
|
$cache = array('offset' => $this->offset);
|
|
$cache += self::unpack('Vcrc32', $this->read(4));
|
|
$cache['size'] = $this->getVarInt();
|
|
$cache['flags'] = $this->getVarInt();
|
|
$cache['quick_offset'] = $this->getVarInt();
|
|
$cache['data_size'] = $this->getVarInt();
|
|
$cache['next'] = $this->offset + $cache['data_size'];
|
|
|
|
// Process the cached header data
|
|
$data = $this->getNextBlockR50();
|
|
$this->processBlockR50($data, true);
|
|
$cache['data'] = $data;
|
|
|
|
// Store the cached data
|
|
$block['cache_data'][] = $cache;
|
|
if ($this->offset != $cache['next']) {
|
|
$this->seek($cache['next']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes the extra records of a RAR 5.0 block passed by reference.
|
|
*
|
|
* @param array $block the block to process
|
|
* @return void
|
|
*/
|
|
protected function processExtraRecords(&$block)
|
|
{
|
|
$end = $this->offset + $block['extra_size'];
|
|
while ($this->offset < $end) {
|
|
|
|
// Start with the record size and type
|
|
$rec = array(
|
|
'name' => 'Unknown',
|
|
'offset' => $this->offset,
|
|
'size' => $size = $this->getVarInt(),
|
|
'next' => $this->offset + $size,
|
|
'type' => $this->getVarInt(),
|
|
);
|
|
|
|
// Block type: ARCHIVE
|
|
if ($block['head_type'] == self::R50_BLOCK_MAIN) {
|
|
if ($rec['type'] == self::R50_MEXTRA_LOCATOR) {
|
|
$rec['name'] = 'Locator';
|
|
$rec['flags'] = $this->getVarInt();
|
|
|
|
if ($rec['flags'] & self::R50_MEXTRA_LOC_QLIST) {
|
|
$rec['quick'] = $this->getVarInt();
|
|
}
|
|
if ($rec['flags'] & self::R50_MEXTRA_LOC_RR) {
|
|
$rec['rr'] = $this->getVarInt();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Block type: FILE
|
|
elseif ($block['head_type'] == self::R50_BLOCK_FILE) {
|
|
if ($rec['type'] == self::R50_FEXTRA_CRYPT) {
|
|
$rec['name'] = 'File encryption';
|
|
$block['has_password'] = true;
|
|
}
|
|
elseif ($rec['type'] == self::R50_FEXTRA_HASH) {
|
|
$rec['name'] = 'File hash';
|
|
}
|
|
elseif ($rec['type'] == self::R50_FEXTRA_HTIME) {
|
|
$rec['name'] = 'File time';
|
|
$rec['flags'] = $this->getVarInt();
|
|
$rec['unix'] = (bool) ($rec['flags'] & self::R50_FEXTRA_HT_UNIX);
|
|
|
|
if ($rec['flags'] & self::R50_FEXTRA_HT_MTIME) {
|
|
if ($rec['unix']) {
|
|
$rec += self::unpack('Vmtime', $this->read(4));
|
|
$block['utime'] = $rec['mtime'];
|
|
} else {
|
|
$rec += self::unpack('Vmtime/Vmtime_high', $this->read(8));
|
|
$block['utime'] = self::win2unixtime($rec['mtime'], $rec['mtime_high']);
|
|
}
|
|
}
|
|
if ($rec['flags'] & self::R50_FEXTRA_HT_CTIME) {
|
|
if ($rec['unix']) {
|
|
$rec += self::unpack('Vctime', $this->read(4));
|
|
} else {
|
|
$rec += self::unpack('Vctime/Vctime_high', $this->read(8));
|
|
}
|
|
}
|
|
if ($rec['flags'] & self::R50_FEXTRA_HT_ATIME) {
|
|
if ($rec['unix']) {
|
|
$rec += self::unpack('Vatime', $this->read(4));
|
|
} else {
|
|
$rec += self::unpack('Vatime/Vatime_high', $this->read(8));
|
|
}
|
|
}
|
|
}
|
|
elseif ($rec['type'] == self::R50_FEXTRA_VERSION) {
|
|
$rec['name'] = 'File version';
|
|
}
|
|
elseif ($rec['type'] == self::R50_FEXTRA_REDIR) {
|
|
$rec['name'] = 'Redirection';
|
|
}
|
|
elseif ($rec['type'] == self::R50_FEXTRA_UOWNER) {
|
|
$rec['name'] = 'Unix owner';
|
|
}
|
|
elseif ($rec['type'] == self::R50_FEXTRA_SUBDATA) {
|
|
$rec['name'] = 'Service data';
|
|
}
|
|
}
|
|
|
|
// Add the extra record
|
|
$block['extra'][] = $rec;
|
|
if ($this->offset != $rec['next']) {
|
|
$this->seek($rec['next']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads a RAR 5.0 variable length integer value from the current offset,
|
|
* which may be an unsigned integer or float depending on the size and system.
|
|
*
|
|
* The high bit of each byte in the little-endian sequence is a continuation
|
|
* flag (where 0 = end of the sequence), the remaining 7 bits are the value.
|
|
* The maximum value is an unsigned 64-bit integer in a 10-byte sequence.
|
|
*
|
|
* @return integer|float the variable length value, or zero on under/overflow
|
|
*/
|
|
protected function getVarInt()
|
|
{
|
|
$shift = $low = $high = 0;
|
|
$count = 1;
|
|
|
|
while ($this->offset < $this->length && $count <= 10) {
|
|
$byte = ord($this->read(1));
|
|
if ($count < 5) {
|
|
$low += ($byte & 0x7f) << $shift;
|
|
} elseif ($count == 5) {
|
|
$low += ($byte & 0x0f) << $shift; // 4 bits
|
|
$high += ($byte >> 4) & 0x07; // 3 bits
|
|
$shift = -4;
|
|
} elseif ($count > 5) {
|
|
$high += ($byte & 0x7f) << $shift;
|
|
}
|
|
if (($byte & 0x80) === 0) {
|
|
if ($low < 0) {$low += 0x100000000;}
|
|
if ($high < 0) {$high += 0x100000000;}
|
|
return ($high ? self::int64($low, $high) : $low);
|
|
}
|
|
$shift += 7;
|
|
$count += 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Resets the instance variables before parsing new data.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function reset()
|
|
{
|
|
parent::reset();
|
|
$this->isVolume = false;
|
|
$this->hasAuth = false;
|
|
$this->hasRecovery = false;
|
|
$this->isEncrypted = false;
|
|
$this->blocks = array();
|
|
$this->format = '';
|
|
$this->comments = '';
|
|
}
|
|
|
|
} // End RarInfo class
|
|
|
|
/**
|
|
* RarUnicodeFilename class.
|
|
*
|
|
* This utility class handles the unicode filename decompression for RAR files. It is
|
|
* adapted directly from Marko Kreen's python script rarfile.py.
|
|
*
|
|
* @link https://github.com/markokr/rarfile
|
|
*
|
|
* @version 1.2
|
|
*/
|
|
class RarUnicodeFilename
|
|
{
|
|
/**
|
|
* Initializes the class instance.
|
|
*
|
|
* @param string $stdName the standard filename
|
|
* @param string $encData the unicode data
|
|
* @return void
|
|
*/
|
|
public function __construct($stdName, $encData)
|
|
{
|
|
$this->stdName = $stdName;
|
|
$this->encData = $encData;
|
|
}
|
|
|
|
/**
|
|
* Decompresses the unicode filename by combining the standard filename with
|
|
* the additional unicode data, return value is encoded as UTF-16LE.
|
|
*
|
|
* @return mixed the unicode filename, or false on failure
|
|
*/
|
|
public function decode()
|
|
{
|
|
$highByte = $this->encByte();
|
|
$encDataLen = strlen($this->encData);
|
|
$flagBits = 0;
|
|
|
|
while ($this->encPos < $encDataLen) {
|
|
if ($flagBits == 0) {
|
|
$flags = $this->encByte();
|
|
$flagBits = 8;
|
|
}
|
|
$flagBits -= 2;
|
|
|
|
switch (($flags >> $flagBits) & 3) {
|
|
case 0:
|
|
$this->put($this->encByte(), 0);
|
|
break;
|
|
case 1:
|
|
$this->put($this->encByte(), $highByte);
|
|
break;
|
|
case 2:
|
|
$this->put($this->encByte(), $this->encByte());
|
|
break;
|
|
default:
|
|
$n = $this->encByte();
|
|
if ($n & 0x80) {
|
|
$c = $this->encByte();
|
|
for ($i = 0; $i < (($n & 0x7f) + 2); $i++) {
|
|
$lowByte = ($this->stdByte() + $c) & 0xFF;
|
|
$this->put($lowByte, $highByte);
|
|
}
|
|
} else {
|
|
for ($i = 0; $i < ($n + 2); $i++) {
|
|
$this->put($this->stdByte(), 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the unicode string
|
|
if ($this->failed) {return false;}
|
|
return $this->output;
|
|
}
|
|
|
|
/**
|
|
* The standard filename data.
|
|
* @var string
|
|
*/
|
|
protected $stdName;
|
|
|
|
/**
|
|
* The unicode data used for processing.
|
|
* @var string
|
|
*/
|
|
protected $encData;
|
|
|
|
/**
|
|
* Pointer for the standard filename data.
|
|
* @var integer
|
|
*/
|
|
protected $pos = 0;
|
|
|
|
/**
|
|
* Pointer for the unicode data.
|
|
* @var integer
|
|
*/
|
|
protected $encPos = 0;
|
|
|
|
/**
|
|
* Did the decompression fail?
|
|
* @var boolean
|
|
*/
|
|
protected $failed = false;
|
|
|
|
/**
|
|
* Decompressed unicode filename string.
|
|
* @var string
|
|
*/
|
|
protected $output;
|
|
|
|
/**
|
|
* Gets the current byte value from the unicode data and increments the
|
|
* pointer if successful.
|
|
*
|
|
* @return integer encoded byte value, or 0 on fail
|
|
*/
|
|
protected function encByte()
|
|
{
|
|
if (isset($this->encData[$this->encPos])) {
|
|
$ret = ord($this->encData[$this->encPos]);
|
|
} else {
|
|
$this->failed = true;
|
|
$ret = 0;
|
|
}
|
|
$this->encPos++;
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Gets the current byte value from the standard filename data.
|
|
*
|
|
* @return integer standard byte value, or placeholder on fail
|
|
*/
|
|
protected function stdByte()
|
|
{
|
|
if (isset($this->stdName[$this->pos])) {
|
|
return ord($this->stdName[$this->pos]);
|
|
}
|
|
$this->failed = true;
|
|
return ord('?');
|
|
}
|
|
|
|
/**
|
|
* Builds the output for the unicode filename string in 16-bit blocks (UTF-16LE).
|
|
*
|
|
* @param integer $low low byte value
|
|
* @param integer $high high byte value
|
|
* @return void
|
|
*/
|
|
protected function put($low, $high)
|
|
{
|
|
$this->output .= chr($low);
|
|
$this->output .= chr($high);
|
|
$this->pos++;
|
|
}
|
|
|
|
} // End RarUnicodeFilename class
|