From 266f52ac2eb2d78a0848071a6985a54e48abd647 Mon Sep 17 00:00:00 2001 From: niel Date: Sat, 15 Oct 2016 09:05:13 +0100 Subject: [PATCH] Remove RARInfo from our maintained code now that the library has a composer.json file. --- composer.json | 7 +- libs/zeebinz/rarinfo/archiveinfo.php | 886 ------------- libs/zeebinz/rarinfo/archivereader.php | 769 ------------ libs/zeebinz/rarinfo/par2info.php | 454 ------- libs/zeebinz/rarinfo/pipereader.php | 246 ---- libs/zeebinz/rarinfo/rarinfo.php | 1583 ------------------------ libs/zeebinz/rarinfo/sfvinfo.php | 167 --- libs/zeebinz/rarinfo/srrinfo.php | 332 ----- libs/zeebinz/rarinfo/szipinfo.php | 1509 ---------------------- libs/zeebinz/rarinfo/zipinfo.php | 790 ------------ 10 files changed, 6 insertions(+), 6737 deletions(-) delete mode 100755 libs/zeebinz/rarinfo/archiveinfo.php delete mode 100755 libs/zeebinz/rarinfo/archivereader.php delete mode 100755 libs/zeebinz/rarinfo/par2info.php delete mode 100755 libs/zeebinz/rarinfo/pipereader.php delete mode 100755 libs/zeebinz/rarinfo/rarinfo.php delete mode 100755 libs/zeebinz/rarinfo/sfvinfo.php delete mode 100755 libs/zeebinz/rarinfo/srrinfo.php delete mode 100755 libs/zeebinz/rarinfo/szipinfo.php delete mode 100755 libs/zeebinz/rarinfo/zipinfo.php diff --git a/composer.json b/composer.json index 0b93336cda..aee22c82a9 100755 --- a/composer.json +++ b/composer.json @@ -62,6 +62,10 @@ { "type": "vcs", "url": "https://github.com/unionofrad/manual" + }, + { + "type": "vcs", + "url": "https://github.com/zeebinz/rarinfo" } ], "require": { @@ -97,6 +101,8 @@ "php": ">=5.6.9", "phpmailer/phpmailer": "^5.2.14", "smarty/smarty": "~3.1.29", + "unionofrad/lithium": "^1.1-dev", + "zeebinx/rarinfo": "dev-master", "bower-asset/animate.css": "~3.5.1", "bower-asset/autosize-3.0.x": "~3.0.15", "bower-asset/bootstrap-3.3.x": "~3.3.6", @@ -121,7 +127,6 @@ "bower-asset/responsive-tables-js-1.0.x": "~1.0.6", "bower-asset/select2-4.0.x": "~4.0.2", "bower-asset/slimScroll-1.3.x": "~1.3.7", - "unionofrad/lithium": "^1.1-dev", "bower-asset/tinymce-builded": "^4.4" }, "require-dev": { diff --git a/libs/zeebinz/rarinfo/archiveinfo.php b/libs/zeebinz/rarinfo/archiveinfo.php deleted file mode 100755 index 76f54c6dc0..0000000000 --- a/libs/zeebinz/rarinfo/archiveinfo.php +++ /dev/null @@ -1,886 +0,0 @@ - - * - * // Load the archive file or data - * $archive = new ArchiveInfo; - * $archive->open('./foo.rar'); // or $archive->setData($data); - * if ($archive->error) { - * echo "Error: {$archive->error}\n"; - * exit; - * } - * - * // Check the archive type - * if ($archive->type != ArchiveInfo::TYPE_RAR) { - * echo "Source is not a RAR archive\n"; - * // exit here or continue with duck typing - * } - * - * // Check encryption - * if (!empty($archive->isEncrypted)) { - * echo "Archive is password encrypted\n"; - * exit; - * } - * - * // List the contents of all archives recursively - * foreach ($archive->getArchiveFileList() as $file) { - * if (isset($file['error'])) { - * echo "Error: {$file['error']} (in: {$file['source']})\n"; - * continue; // skip recursion errors - * } - * if (!empty($file['pass'])) { - * echo "File is passworded: {$file['name']} (in: {$file['source']})\n"; - * continue; // skip encrypted files - * } - * if (empty($file['compressed'])) { - * echo "Extracting uncompressed file: {$file['name']} from: {$file['source']}\n"; - * $archive->saveFileData($file['name'], "./dir/{$file['name']}", $file['source']); - * // or $data = $archive->getFileData($file['name'], $file['source']); - * } - * } - * - * - * - * @author Hecks - * @copyright (c) 2010-2013 Hecks - * @license Modified BSD - * @version 2.3 - */ -class ArchiveInfo extends ArchiveReader -{ - /**#@+ - * Supported archive types. - */ - const TYPE_NONE = 0x0000; - const TYPE_RAR = 0x0002; - const TYPE_ZIP = 0x0004; - const TYPE_SRR = 0x0008; - const TYPE_SFV = 0x0010; - const TYPE_PAR2 = 0x0020; - const TYPE_SZIP = 0x0040; - - /**#@-*/ - - /** - * Source path label of the main archive. - */ - const MAIN_SOURCE = 'main'; - - /** - * The default list of the supported archive reader classes. - * @var array - */ - protected $readers = array( - self::TYPE_RAR => 'RarInfo', - self::TYPE_SRR => 'SrrInfo', - self::TYPE_PAR2 => 'Par2Info', - self::TYPE_ZIP => 'ZipInfo', - self::TYPE_SFV => 'SfvInfo', - self::TYPE_SZIP => 'SzipInfo', - ); - - /** - * The current archive file/data type. - * @var integer - */ - public $type = self::TYPE_NONE; - - /** - * Sets the list of supported archive reader classes for the current instance, - * overriding the defaults. The keys should be valid archive types: - * - * $archive->setReaders(array(ArchiveInfo::TYPE_RAR => 'RarInfo')); - * - * If $recursive is set to true, this list will also be used for all embedded - * archives as well. This can be a bit unpredictable, so use with caution, but - * it's a convenient way to swap in custom readers or change their order. - * - * @param array $readers list of reader classes keyed by archive type - * @param boolean $recursive apply list to all embedded archives? - * @return void - */ - public function setReaders(array $readers, $recursive=false) - { - $this->readers = $readers; - $this->inheritReaders = $recursive; - if ($recursive) { - $this->archives = array(); - } - } - - /** - * Sets the list of external clients configured for each reader type to allow - * extraction of compressed files. The keys should be valid archive types: - * - * $archive->setExternalClients(array( - * ArchiveInfo::TYPE_RAR => 'path_to_unrar_client', - * ArchiveInfo::TYPE_ZIP => 'path_to_unzip_client', - * )); - * - * Note that extracting embedded encrypted files is not currently supported - * due to recursion issues. - * - * @param array $clients list of external clients - * @return void - */ - public function setExternalClients(array $clients) - { - if ($this->reader && isset($clients[$this->type])) { - $this->reader->setExternalClient($clients[$this->type]); - } - - $this->externalClients = $clients; - $this->archives = array(); - if ($this->reader) { - unset($this->error); - } - } - - /** - * Sets the regex string (minus delimiters and brackets) for filtering valid - * archive extensions when inspecting archive contents recursively. This check - * can be disabled completely by setting the value to NULL. - * - * @param string $extensions the regex of archive file extensions - * @return void - */ - public function setArchiveExtensions($extensions) - { - $this->extensions = $extensions; - $this->archives = array(); - } - - /** - * Convenience method that outputs a summary list of the archive information, - * useful for pretty-printing. - * - * When called with $full set to true, this method will also return a nested - * summary of all the embedded archive contents in the 'archives' field, keyed - * to the archive filenames. - * - * @param boolean $full return a full summary? - * @return array archive summary - */ - public function getSummary($full=false) - { - $summary = array( - 'main_info' => isset($this->readers[$this->type]) ? $this->readers[$this->type] : 'Unknown', - 'main_type' => $this->type, - 'file_name' => $this->file, - 'file_size' => $this->fileSize, - 'data_size' => $this->dataSize, - 'use_range' => "{$this->start}-{$this->end}", - ); - if ($this->reader) { - $args = func_get_args(); - $summary += $this->__call('getSummary', $args); - } - if ($full && $this->containsArchive()) { - $summary['archives'] = $this->getArchiveList(true); // recursive - } - if ($this->tempFiles) { - $summary['temp_files'] = array_keys($this->tempFiles); - } - if ($this->error) { - $summary['error'] = $this->error; - } - return $summary; - } - - /** - * Returns a list of records for each of the files in the archive by delegation - * to the stored reader. - * - * @return array|boolean list of file records, or false if none are available - */ - public function getFileList() - { - if ($this->reader) { - $args = func_get_args(); - return $this->__call('getFileList', $args); - } - - return false; - } - - /** - * Returns a list of the parsed data found in the source in human-readable - * format (for debugging purposes only). - * - * @return array|boolean parsed data, or false if none available - */ - public function getParsedData() - { - switch ($this->type) { - case self::TYPE_RAR: - case self::TYPE_SRR: - return $this->reader->getBlocks(); - case self::TYPE_PAR2: - return $this->reader->getPackets(); - case self::TYPE_ZIP: - return $this->reader->getRecords(); - case self::TYPE_SZIP: - return $this->reader->getHeaders(); - case self::TYPE_SFV: - return $this->reader->getFileList(); - default: - return false; - } - } - - /** - * Returns the stored archive reader instance for the file/data type. - * - * @return ArchiveReader - */ - public function getReader() - { - return $this->reader; - } - - /** - * Determines whether the current archive type can contain other archives - * through which it can search recursively. - * - * @return boolean - */ - public function allowsRecursion() - { - return (bool) ($this->type & (self::TYPE_RAR | self::TYPE_ZIP | self::TYPE_SZIP)); - } - - /** - * Determines whether the current archive contains another archive. - * - * @return boolean - */ - public function containsArchive() - { - $this->getArchiveList(); - return !empty($this->archives); - } - - /** - * Determines whether the current reader can extract files using an external - * client. - * - * @return boolean - */ - public function canExtract() - { - return $this->reader && (!empty($this->externalClients[$this->type]) - || !empty($this->reader->externalClient)); - } - - /** - * Lists any embedded archives, either as raw ArchiveInfo objects or as file - * summaries, and caches the object list locally. The optional filtering of - * valid archive extensions can be disabled by first calling: - * - * $archive->setArchiveExtensions(null); - * - * This will mean that all files in the archive will be inspected, regardless - * of their extensions - less efficient, more paranoid & probably buggier ;) - * - * @param boolean $summary return file summaries? - * @return array|boolean list of stored objects/summaries, or false on error - */ - public function getArchiveList($summary=false) - { - if (!$this->reader || !$this->allowsRecursion()) - return false; - - if (empty($this->archives)) { - $extensions = !empty($this->extensions) ? "/^({$this->extensions})$/" : false; - foreach ($this->reader->getFileList() as $file) { - if ($extensions && !preg_match($extensions, pathinfo($file['name'], PATHINFO_EXTENSION))) - continue; - if (($archive = $this->getArchive($file['name'])) - && ($archive->type != self::TYPE_NONE || empty($archive->readers)) - ) { - $this->archives[$file['name']] = $archive; - } - } - if (!empty($this->externalClients)) { - $this->extractArchives(); - } - } - if ($summary) { - $ret = array(); - foreach ($this->archives as $name => $archive) { - $ret[$name] = $archive->getSummary(true); // recursive - } - return $ret; - } - - return $this->archives; - } - - /** - * Returns an ArchiveInfo object for an embedded archive file with the contents - * analyzed (initially without recursion). Calls to this method can also be - * chained together to navigate the tree, e.g.: - * - * $rar->getArchive('parent.rar')->getArchive('child.zip')->getFileList(); - * - * @param string $filename the embedded archive filename - * @return ArchiveInfo|boolean false if an object can't be returned - */ - public function getArchive($filename) - { - if (!$this->reader || !$this->allowsRecursion()) - return false; - - // Check the cache first - if (isset($this->archives[$filename])) - return $this->archives[$filename]; - - foreach ($this->reader->getFileList(true) as $file) { - if ($file['name'] == $filename && isset($file['range'])) { - - // Create the new archive object - $archive = new self; - $archive->externalClients = $this->externalClients; - $archive->extensions = $this->extensions; - if ($this->inheritReaders) { - $archive->setReaders($this->readers, true); - } - - // Extract any compressed data to a temporary file if supported - if ($this->canExtract() && !empty($file['compressed']) && empty($file['pass'])) { - list($hash, $temp) = $this->getTempFileName("{$file['name']}:{$file['range']}"); - if (!isset($this->tempFiles[$hash])) { - $this->reader->extractFile($file['name'], $temp); - @chmod($temp, 0777); - $this->tempFiles[$hash] = $temp; - } - if ($this->reader->error) { - $archive->error = $this->reader->error; - $archive->readers = array(); - } else { - $archive->open($temp, $this->isFragment); - $archive->isTemporary = true; - } - return $archive; - } - - // Otherwise we shouldn't process any files that are unreadable - if (!empty($file['compressed']) || !empty($file['pass'])) { - $archive->readers = array(); - } - - // Try to parse the raw source file/data - $range = explode('-', $file['range']); - if ($this->file) { - $archive->open($this->file, $this->isFragment, $range); - } else { - $archive->setData($this->data, $this->isFragment, $range); - } - - // Make error messages more specific - if (!empty($file['compressed'])) { - $archive->error = 'The archive is compressed and cannot be read'; - } - if (!empty($file['pass']) || !empty($archive->isEncrypted)) { - $archive->error = 'The archive is encrypted and cannot be read'; - } - - return $archive; - } - } - - // Something went wrong - return false; - } - - /** - * Provides the contents of the current archive in a flat list, optionally - * recursing through all embedded archives as well, with a 'source' field - * added to each item that includes the archive source path. - * - * If $all is set to true, the file lists of all the supported archive types - * will be merged in the flat list, not just those that allow recursion. This - * should be used with caution, as the output varies between readers and the - * only guaranteed results are: - * - * array('name' => '...', 'source' => '...') or: - * array('error' => '...', 'source' => '...') - * - * It's really just handy for inspecting all known file names in the laziest - * way possible, and not much more than that. - * - * @param boolean $recurse list all archive contents recursively? - * @param string $all include all supported archive file lists? - * @param string $source [ignore, for internal use only] - * @return array|boolean the flat archive file list, or false on error - */ - public function getArchiveFileList($recurse=true, $all=false, $source=null) - { - if (!$this->reader) {return false;} - $ret = array(); - - // Start with the main parent - if ($source == null) { - $source = self::MAIN_SOURCE; - if ($ret = $this->reader->getFileList()) { - $ret = $this->flattenFileList($ret, $source, $all); - } - } - - // Merge each archive file list - if ($recurse && $this->containsArchive()) { - foreach ($this->getArchiveList() as $name => $archive) { - - // Only include the file lists of types that allow recursion? - if (empty($archive->error) && !$all && !$archive->allowsRecursion()) - continue; - $branch = $source.' > '.$name; - - // We should append any errors - if ($archive->error || !($files = $archive->reader->getFileList())) { - $error = $archive->error ? $archive->error : 'No files found'; - $ret[] = array('error' => $error, 'source' => $branch); - continue; - } - - // Otherwise merge recursively - $ret = array_merge($ret, $this->flattenFileList($files, $branch, $all)); - if ($archive->containsArchive()) { - $ret = array_merge($ret, $archive->getArchiveFileList(true, $all, $branch)); - } - } - } - - return $ret; - } - - /** - * Retrieves the archive reader object described by the given source path string. - * - * @param string $source archive source path of the file - * @return ArchiveReader|boolean archive reader object, or false on error - */ - public function getArchiveFromSource($source) - { - $this->getArchiveFileList(true); - $source = explode(' > ', $source); - foreach ($source as $file) { - $archive = ($file == self::MAIN_SOURCE) ? $this : $archive->getArchive($file); - if (!$archive) {break;} - } - - return isset($archive) ? $archive : false; - } - - /** - * Retrieves the raw data for the given filename and optionally the archive - * source (e.g. 'main' or 'main > child.rar', etc.). - * - * @param string $filename name of the file to extract - * @param string $source archive source path of the file - * @return string|boolean file data, or false on error - */ - public function getFileData($filename, $source=self::MAIN_SOURCE) - { - // Check that a valid data source is available - if (!$this->reader || ($this->reader->data == '' && $this->reader->handle == null)) - return false; - - // Get the absolute start/end positions - if (!($info = $this->getFileInfo($filename, $source)) || empty($info['range'])) { - $in_source = $source ? " in: ({$source})" : ''; - $this->error = "Could not find file info for: ({$filename}){$in_source}"; - return false; - } - unset($this->error); - - return $this->reader->getRange(explode('-', $info['range'])); - } - - /** - * Saves the raw data for the given filename and optionally archive source path - * to the given destination (e.g. 'main' or 'main > child.rar', etc.). - * - * @param string $filename name of the file to extract - * @param string $destination full path of the file to create - * @param string $source archive source path of the file - * @return integer|boolean number of bytes saved or false on error - */ - public function saveFileData($filename, $destination, $source=self::MAIN_SOURCE) - { - // Check that a valid data source is available - if (!$this->reader || ($this->reader->data == '' && $this->reader->handle == null)) - return false; - - // Get the absolute start/end positions - if (!($info = $this->getFileInfo($filename, $source)) || empty($info['range'])) { - $in_source = $source ? " in: ({$source})" : ''; - $this->error = "Could not find file info for: ({$filename}){$in_source}"; - return false; - } - unset($this->error); - - return $this->reader->saveRange(explode('-', $info['range']), $destination); - } - - /** - * Extracts a compressed or encrypted file using one of the configured external - * clients, optionally returning the data or saving it to file. - * - * Note that this method will fail with uncompressed or unencrypted files in - * embedded archives - use getFileData() or saveFileData() instead. - * - * @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 - * @param string $source archive source path of the file to extract - * @return mixed extracted data, number of bytes saved or false on error - */ - public function extractFile($filename, $destination=null, $password=null, $source=self::MAIN_SOURCE) - { - // Check that a valid reader is available - if (!($archive = $this->getArchiveFromSource($source)) || !($reader = $archive->reader)) { - $this->error = "Not a valid archive source: {$source}"; - return false; - } - if (!method_exists($reader, 'extractFile')) { - $this->error = get_class($reader).' does not support the extractFile() method'; - return false; - } - - // Get the result of the extraction - $result = $reader->extractFile($filename, $destination, $password); - if ($reader->error) { - $this->error = $reader->error; - return false; - } - unset($this->error); - - return $result; - } - - /** - * Class destructor, cleanly removes any archive reader references. - * - * @return void - */ - public function __destruct() - { - $this->reset(); - parent::__destruct(); - } - - /** - * Returns the position of the first archive marker/signature in the stored - * data or file by delegation to the stored reader. - * - * @return mixed Marker position, or false if marker is missing - */ - public function findMarker() - { - if ($this->reader) - return $this->reader->findMarker(); - - return false; - } - - /** - * Magic method for accessing the properties of the stored reader. - * - * @param string $name the property name - * @return mixed the property value - */ - public function __get($name) - { - if ($this->reader && isset($this->reader->$name)) - return $this->reader->$name; - - return parent::__get($name); - } - - /** - * Magic method for testing whether properties of the stored reader are set. - * Note that if called via empty(), if the method returns TRUE a second call - * is made to __get() to test if the actual value is false. - * - * @param string $name the property name - * @return boolean - */ - public function __isset($name) - { - if ($this->reader) - return isset($this->reader->$name); - - return false; - } - - /** - * Magic method for delegating method calls to the stored reader. - * - * @param string $method the method name - * @param array $args the method arguments - * @return mixed result of the delegated method call - * @throws BadMethodCallException - */ - public function __call($method, $args) - { - if (!$this->reader) - throw new BadMethodCallException(get_class($this)."::$method() is not defined"); - - switch (count($args)) { - case 0: - return $this->reader->$method(); - case 1: - return $this->reader->$method($args[0]); - case 2: - return $this->reader->$method($args[0], $args[1]); - case 3: - return $this->reader->$method($args[0], $args[1], $args[2]); - default: - return call_user_func_array(array($this->reader, $method), $args); - } - } - - /** - * The stored archive reader instance. - * @var ArchiveReader - */ - protected $reader; - - /** - * Cached list of any embedded archive objects. - * @var array - */ - protected $archives = array(); - - /** - * Should any embedded archives inherit the readers list from this instance? - * @var boolean - */ - protected $inheritReaders = false; - - /** - * List of any external clients to use for extraction. - * @var array - */ - protected $externalClients = array(); - - /** - * Is the current archive being processed from a temporary file? - * @var boolean - */ - protected $isTemporary = false; - - /** - * The regex for filtering any valid archive extensions. - * @var string - */ - protected $extensions = 'rar|r[0-9]+|zip|srr|par2|sfv|7z|[0-9]+'; - - /** - * Parses the source file/data by delegation to one of the configured readers, - * or returns false if the source is not a supported type. - * - * @return boolean false if parsing fails - */ - protected function analyze() - { - // Create a reader to handle the file/data - if ($this->createReader() === false || $this->findMarker() === false) { - $this->error = 'Source is not a supported archive type'; - $this->close(); - return false; - } - - // Delegate some properties to the reader - unset($this->markerPosition); - unset($this->fileCount); - unset($this->error); - - // Let the reader handle any files - if ($this->file) { - $this->close(); - } - - return true; - } - - /** - * Creates a reader instance for parsing the source file/data and stores it - * locally for any later delegation. - * - * Each reader in the configured order tries to parse the source file/data and - * find a valid marker/signature for its type. Where more than one marker is - * present - such as with embedded archives - the reader that finds the earliest - * marker will be used as the delegate. - * - * @return boolean false if no reader could parse the source - */ - protected function createReader() - { - $range = array($this->start, $this->end); - - foreach ($this->readers as $type => $class) { - - // Analyze the source with a new reader - $reader = new $class; - if ($this->file) { - $reader->open($this->file, $this->isFragment, $range); - } else { - $reader->setData($this->data, $this->isFragment, $range); - } - if ($reader->error) {continue;} - - // Store the reader with the earliest marker - if (($marker = $reader->findMarker()) !== false) { - $start = !isset($start) ? $marker : $start; - if ($marker <= $start) { - $start = $marker; - if (!empty($this->externalClients[$type])) { - $reader->setExternalClient($this->externalClients[$type]); - } - $this->reader = $reader; - $this->type = $type; - } - if ($start === 0) {break;} - } - } - - return isset($this->reader); - } - - /** - * Returns information for the given filename and optionally archive source - * in the current file/data. - * - * @param string $filename the filename to search - * @param string $source archive source path of the file - * @return array|boolean the file info or false on error - */ - protected function getFileInfo($filename, $source=self::MAIN_SOURCE) - { - if (strpos($source, self::MAIN_SOURCE) !== 0) { - $source = self::MAIN_SOURCE.' > '.$source; - } - foreach ($this->getArchiveFileList(true) as $file) { - if (!empty($file['name']) && empty($file['is_dir']) - && $file['name'] == $filename && $file['source'] == $source - ) { - return $file; - } - } - - return false; - } - - /** - * Helper method that flattens a file list that may have children, removes keys - * and re-indexes, then adds a source path field to each item. - * - * @param array $files the file list to flatten - * @param string $source the current source path info - * @param boolean $all should any child lists be included? - * @return array the flat file list - */ - protected function flattenFileList(array $files, $source, $all=false) - { - $files = array_values($files); - $children = array(); - foreach ($files as &$file) { - $file['source'] = $source; - if ($source != self::MAIN_SOURCE) { - unset($file['next_offset']); - } - if ($all && !empty($file['files'])) foreach ($file['files'] as $child) { - $child['source'] = $source.' > '.$file['name']; - unset($child['next_offset']); - $children[] = $child; - } - } - if (!empty($children)) { - $files = array_merge($files, $children); - } - - return $files; - } - - /** - * Extracts any embedded archives that contain compressed files using the - * configured external clients to allow recursive inspection and extraction. - * - * @return boolean false on error - */ - protected function extractArchives() - { - if (!$this->reader || !$this->canExtract()) - return false; - - foreach ($this->archives as $name => $archive) { - if ($archive->isTemporary || !$archive->canExtract()) - continue; - - if ($files = $archive->reader->getFileList()) foreach ($files as $file) { - if (!empty($file['compressed']) && empty($file['pass'])) { - list($hash, $temp) = $this->getTempFileName("{$name}:{$archive->start}-{$archive->end}"); - if (!isset($this->tempFiles[$hash])) { - $archive->reader->saveRange(array($archive->start, $archive->end), $temp); - @chmod($temp, 0777); - $this->tempFiles[$hash] = $temp; - } - $archive->open($temp, $this->isFragment); - $archive->isTemporary = true; - continue 2; - } - } - } - - return true; - } - - /** - * Resets the instance variables before parsing new data. - * - * @return void - */ - protected function reset() - { - $this->reader = null; - foreach ($this->archives as $archive) { - $archive->reset(); - } - parent::reset(); - $this->archives = array(); - $this->type = self::TYPE_NONE; - $this->isTemporary = false; - } - -} // End ArchiveInfo class diff --git a/libs/zeebinz/rarinfo/archivereader.php b/libs/zeebinz/rarinfo/archivereader.php deleted file mode 100755 index 75ae1ff9cb..0000000000 --- a/libs/zeebinz/rarinfo/archivereader.php +++ /dev/null @@ -1,769 +0,0 @@ - $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 diff --git a/libs/zeebinz/rarinfo/par2info.php b/libs/zeebinz/rarinfo/par2info.php deleted file mode 100755 index 05866d1c4b..0000000000 --- a/libs/zeebinz/rarinfo/par2info.php +++ /dev/null @@ -1,454 +0,0 @@ - - * - * // Load the PAR2 file or data - * $par2 = new Par2Info; - * $par2->open('./foo.par2'); // or $par2->setData($data); - * if ($par2->error) { - * echo "Error: {$par2->error}\n"; - * exit; - * } - * - * // Process the recovery set file list & hashes - * $files = $par2->getFileList(); - * foreach ($files as $fileID => $file) { - * echo "Input file: {$file['name']} ({$file['size']}):\n"; - * echo "-- MD5 hash: {$file['hash']}:\n"; - * echo "-- MD5 hash (16KB): {$file['hash_16K']}:\n"; - * } - * } - * - * - * - * @link http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html - * - * @author Hecks - * @copyright (c) 2010-2013 Hecks - * @license Modified BSD - * @version 1.7 - */ -class Par2Info extends ArchiveReader -{ - // ------ Class constants ----------------------------------------------------- - - /**#@+ - * PAR2 file format values - */ - - // Packet Marker - const PACKET_MARKER = "PAR2\x00PKT"; - - // Core packet types - const PACKET_MAIN = "PAR 2.0\x00Main\x00\x00\x00\x00"; - const PACKET_FILEDESC = "PAR 2.0\x00FileDesc"; - const PACKET_FILEVER = "PAR 2.0\x00IFSC\x00\x00\x00\x00"; - const PACKET_RECOVERY = "PAR 2.0\x00RecvSlic"; - const PACKET_CREATOR = "PAR 2.0\x00Creator\x00"; - - // Optional packet types - const PACKET_FILENAME_UC = "PAR 2.0\x00UniFileN"; - const PACKET_COMMENT_ASCII = "PAR 2.0\x00CommASCI"; - const PACKET_COMMENT_UC = "PAR 2.0\x00CommUni\x00"; - const PACKET_INPUT_BLOCK = "PAR 2.0\x00FileSlic"; - const PACKET_RECOVERY_VER = "PAR 2.0\x00RFSC\x00\x00\x00\x00"; - const PACKET_PACKED_MAIN = "PAR 2.0\x00PkdMain\x00"; - const PACKET_PACKED_RECOVERY = "PAR 2.0\x00PkdRecvS"; - - /**#@-*/ - - /** - * Format for unpacking each PAR2 packet header, in standard and Perl-compatible - * (PHP >= 5.5.0) versions. - */ - const FORMAT_PACKET_HEADER = 'A8head_marker/Vhead_length/Vhead_length_high/H32head_hash/H32head_set_id/A16head_type'; - const PL_FORMAT_PACKET_HEADER = 'a8head_marker/Vhead_length/Vhead_length_high/H32head_hash/H32head_set_id/a16head_type'; - - /** - * Format for unpacking the body of a Main packet. - */ - const FORMAT_PACKET_MAIN = 'Vblock_size/Vblock_size_high/Vrec_file_count'; - - /** - * Format for unpacking the body of a File Description packet. - */ - const FORMAT_PACKET_FILEDESC = 'H32file_id/H32file_hash/H32file_hash_16K/Vfile_length/Vfile_length_high'; - - - // ------ Instance variables and methods --------------------------------------- - - /** - * List of packet names corresponding to packet types. - * @var array - */ - protected $packetNames = array( - self::PACKET_MAIN => 'Main', - self::PACKET_FILEDESC => 'File Description', - self::PACKET_FILEVER => 'File Block Verification', - self::PACKET_RECOVERY => 'Recovery Block', - self::PACKET_CREATOR => 'Creator', - // Optional - self::PACKET_FILENAME_UC => 'Unicode Filename', - self::PACKET_COMMENT_ASCII => 'Comment ASCII', - self::PACKET_COMMENT_UC => 'Comment Unicode', - self::PACKET_INPUT_BLOCK => 'Input File Block', - self::PACKET_RECOVERY_VER => 'Recovery File Block Verification', - self::PACKET_PACKED_MAIN => 'Packed Main', - self::PACKET_PACKED_RECOVERY => 'Packed Recovery', - ); - - /** - * Number of valid recovery blocks in the file/data. - * @var integer - */ - public $blockCount = 0; - - /** - * Size in bytes of the recovery blocks. - * @var integer - */ - public $blockSize = 0; - - /** - * Details of the client that created the PAR2 file/data. - * @var string - */ - public $client = ''; - - /** - * Convenience method that outputs a summary list of the file/data information, - * useful for pretty-printing. - * - * @param boolean $full add file list to output? - * @return array file/data summary - */ - public function getSummary($full=false) - { - $summary = array( - 'file_name' => $this->file, - 'file_size' => $this->fileSize, - 'data_size' => $this->dataSize, - 'client' => $this->client, - 'block_count' => $this->blockCount, - 'block_size' => $this->blockSize, - 'file_count' => $this->fileCount, - ); - if ($full) { - $summary['file_list'] = $this->getFileList(); - } - if ($this->error) { - $summary['error'] = $this->error; - } - - return $summary; - } - - /** - * Returns a list of the PAR2 packets found in the file/data in human-readable - * format (for debugging purposes only). - * - * @param boolean $full include all packet details in output? - * @return array|boolean list of packets, or false if none available - */ - public function getPackets($full=false) - { - // Check that packets are stored - if (empty($this->packets)) {return false;} - - // Build the packet list - $ret = array(); - - foreach ($this->packets AS $packet) { - - // File Block Verification packets are very verbose - if (!$full && $packet['head_type'] == self::PACKET_FILEVER) {continue;} - - $p = array(); - $p['type_name'] = isset($this->packetNames[$packet['head_type']]) ? $this->packetNames[$packet['head_type']] : 'Unknown'; - $p += $packet; - - // Sanity check filename length - if (isset($p['file_name'])) {$p['file_name'] = substr($p['file_name'], 0, $this->maxFilenameLength);} - $ret[] = $p; - } - - return $ret; - } - - /** - * Parses the stored packets and returns a list of records for each of the - * files in the recovery set. - * - * @return array list of file records, empty if none are available - */ - public function getFileList() - { - $ret = array(); - foreach ($this->packets as $packet) { - if ($packet['head_type'] == self::PACKET_FILEDESC && !isset($ret[$packet['file_id']])) { - $ret[$packet['file_id']] = $this->getFilePacketSummary($packet); - } - if ($packet['head_type'] == self::PACKET_FILEVER && empty($ret[$packet['file_id']]['blocks'])) { - $ret[$packet['file_id']]['blocks'] = count($packet['block_checksums']); - } - } - - return $ret; - } - - /** - * List of File IDs found in the file/data. - * @var array - */ - protected $fileIDs = array(); - - /** - * List of packets found in the file/data. - * @var array - */ - protected $packets = array(); - - /** - * Returns a processed summary of a PAR2 File Description packet. - * - * @param array $packet a valid File Description packet - * @return array summary information - */ - protected function getFilePacketSummary($packet) - { - return array( - 'name' => !empty($packet['file_name']) ? substr($packet['file_name'], 0, $this->maxFilenameLength) : 'Unknown', - 'size' => isset($packet['file_length']) ? $packet['file_length'] : 0, - 'hash' => $packet['file_hash'], - 'hash_16K' => $packet['file_hash_16K'], - 'blocks' => 0, - 'next_offset' => $packet['next_offset'], - ); - } - - /** - * Returns the position of the first PAR2 Packet Marker string in the file/data. - * - * @return mixed Marker position, or false if none found - */ - public function findMarker() - { - if ($this->markerPosition !== null) - return $this->markerPosition; - - try { - $buff = $this->read(min($this->length, $this->maxReadBytes)); - $this->rewind(); - return $this->markerPosition = strpos($buff, self::PACKET_MARKER); - } catch (Exception $e) { - return false; - } - } - - /** - * Parses the PAR2 data and stores a list of valid packets locally. - * - * @return boolean false if parsing fails - */ - protected function analyze() - { - // Find the first Packet Marker, if there is one - if (($startPos = $this->findMarker()) === false) { - $this->error = 'Could not find a Packet Marker, not a valid PAR2 file'; - return false; - } - $this->seek($startPos); - - // Analyze all packets - while ($this->offset < $this->length) try { - - // Get the next packet header - $packet = $this->getNextPacket(); - - // Verify the packet - if ($this->verifyPacket($packet) === false) { - $this->error = "Packet failed checksum (offset: {$this->offset})"; - throw new Exception('Packet checksum failed'); - } - - // Process the current packet by type - $this->processPacket($packet); - - // Add the current packet to the list - $this->packets[] = $packet; - - // Skip to the next packet, if any - if ($this->offset != $packet['next_offset']) { - $this->seek($packet['next_offset']); - } - - // Sanity check - if ($packet['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 packets - if (empty($this->packets)) { - $this->error = 'No valid PAR2 packets were found'; - $this->close(); - return false; - } - - // Analysis was successful - $this->close(); - return true; - } - - /** - * Reads the start of the next packet header and returns the common packet - * info before further processing by packet type. - * - * @return array the next packet header info - */ - protected function getNextPacket() - { - // Start the packet info - $packet = array('offset' => $this->offset); - - // Unpack the packet header - $format = (version_compare(PHP_VERSION, '5.5.0') >= 0) - ? self::PL_FORMAT_PACKET_HEADER - : self::FORMAT_PACKET_HEADER; - $packet += self::unpack($format, $this->read(64)); - - // Convert packet size (64-bit integer) - $packet['head_length'] = self::int64($packet['head_length'], $packet['head_length_high']); - - // Add offset info for next packet (if any) - $packet['next_offset'] = $packet['offset'] + $packet['head_length']; - - // Return the packet info - return $packet; - } - - /** - * Verifies that the given packet is valid and parsable. - * - * @param array $packet the packet to verify - * @return boolean false on failure - */ - protected function verifyPacket($packet) - { - $offset = $this->offset; - - // Check the MD5 hash of the data from head_set_id to packet end - $this->seek($packet['offset'] + 32); - $data = $this->read($packet['head_length'] - 32); - $this->seek($offset); - - return (md5($data) === $packet['head_hash']); - } - - /** - * Processes a packet passed by reference and unpacks its body. - * - * @param array $packet the packet to process - * @return void - */ - protected function processPacket(&$packet) - { - // Packet type: MAIN - if ($packet['head_type'] == self::PACKET_MAIN) { - $packet += self::unpack(self::FORMAT_PACKET_MAIN, $this->read(12)); - $packet['block_size'] = self::int64($packet['block_size'], $packet['block_size_high']); - $this->blockSize = $packet['block_size']; - - // Unpack the File IDs of all files in the recovery set - $recoverable = array(); - for ($i = 0; $i < $packet['rec_file_count']; $i++) { - $recoverable = array_merge($recoverable, self::unpack('H32', $this->read(16))); - } - $packet['rec_file_ids'] = $recoverable; - - // Unpack any File IDs of files not in the recovery set - $unrecoverable = array(); - while ($this->offset < $packet['next_offset']) { - $unrecoverable = array_merge($unrecoverable, self::unpack('H32', $this->read(16))); - } - if (!empty($unrecoverable)) { - $packet['other_file_ids'] = $unrecoverable; - } - } - - // Packet type: FILE DESCRIPTION - elseif ($packet['head_type'] == self::PACKET_FILEDESC) { - $packet += self::unpack(self::FORMAT_PACKET_FILEDESC, $this->read(56)); - $packet['file_length'] = self::int64($packet['file_length'], $packet['file_length_high']); - $len = ($packet['offset'] + $packet['head_length']) - $this->offset; - $packet['file_name'] = rtrim($this->read($len)); - - // Add ID to the stored file list so we don't double-count - if (!isset($this->fileIDs[$packet['file_id']])) { - $this->fileIDs[$packet['file_id']] = true; - $this->fileCount++; - } - } - - // Packet type: FILE BLOCK VERIFICATION - elseif ($packet['head_type'] == self::PACKET_FILEVER) { - $packet += self::unpack('H32file_id', $this->read(16)); - - // Unpack the MD5/CRC32 checksum pairs - $packet['block_checksums'] = array(); - while ($this->offset < $packet['next_offset']) { - $packet['block_checksums'][] = self::unpack('H32md5/Vcrc32', $this->read(20)); - } - } - - // Packet type: RECOVERY BLOCK - elseif ($packet['head_type'] == self::PACKET_RECOVERY) { - $packet += self::unpack('Vexponent', $this->read(4)); - $this->blockCount++; - } - - // Packet type: CREATOR - elseif ($packet['head_type'] == self::PACKET_CREATOR) { - $len = ($packet['offset'] + $packet['head_length']) - $this->offset; - $packet['client'] = $this->read($len); - $this->client = rtrim($packet['client']); - } - } - - /** - * Resets the instance variables before parsing new data. - * - * @return void - */ - protected function reset() - { - parent::reset(); - - $this->client = ''; - $this->blockCount = 0; - $this->blockSize = 0; - $this->fileIDs = array(); - $this->packets = array(); - } - -} // End Par2Info class diff --git a/libs/zeebinz/rarinfo/pipereader.php b/libs/zeebinz/rarinfo/pipereader.php deleted file mode 100755 index 81aed3627f..0000000000 --- a/libs/zeebinz/rarinfo/pipereader.php +++ /dev/null @@ -1,246 +0,0 @@ - errors.txt' to the command) or by combining with STDOUT ('2>&1'). - * Then the application must either read the file or parse the output for errors. - * - * @author Hecks - * @copyright (c) 2010-2013 Hecks - * @license Modified BSD - * @version 1.2 - */ -class PipeReader -{ - /** - * The last error message. - * @var string - */ - public $error = ''; - - /** - * The last process exit code. - * @var integer - */ - public $exitCode = 0; - - /** - * Default constructor for opening a pipe. - * - * @param string $command the command to execute - * @return void - */ - public function __construct($command=null) - { - if ($command) $this->open($command); - } - - /** - * Opens a pipe to stream the output of a given command. - * - * Note that it's the responsibility of the calling application to sanitize - * the command with e.g. escapeshellcmd(), escapeshellarg(), etc. - * - * @param string $command the command to execute - * @return boolean false if the pipe could not be openend - */ - public function open($command) - { - $this->reset(); - - if (!($handle = popen($command, 'rb'))) { - $this->error = "Could not execute command: ($command)"; - return false; - } - - $this->handle = $handle; - $this->command = $command; - - return true; - } - - /** - * Closes any open pipe handle and sets the exit code. - * - * @return void - */ - public function close() - { - if (is_resource($this->handle)) { - $this->exitCode = pclose($this->handle); - } - $this->handle = null; - } - - /** - * Reads the given number of bytes from the piped command output and moves the - * offset pointer forward, with optional confirmation that the requested bytes - * are available. - * - * @param integer $num number of bytes to read - * @param booleann $confirm check available bytes? - * @return string the byte string - * @throws InvalidArgumentException - */ - public function read($num, $confirm=true) - { - if ($num < 1) { - throw new InvalidArgumentException("Could not read {$num} bytes from offset {$this->offset}"); - } elseif ($num == 0) { - return ''; - } - - // Read the requested bytes - if ($this->command && is_resource($this->handle)) { - $read = ''; $rlen = $num; - while ($rlen > 0 && !feof($this->handle)) { - $data = fread($this->handle, min($this->maxReadBytes, $rlen)); - $rlen -= strlen($data); - $read .= $data; - } - } - - // Confirm the read length? - if ($confirm && (!isset($read) || strlen($read) < $num)) { - $this->offset = $this->tell(); - throw new InvalidArgumentException("Could not read {$num} bytes from offset {$this->offset}"); - } - - // Move the data pointer - $this->offset = $this->tell(); - - return isset($read) ? $read : ''; - } - - /** - * Convenience method for reading the remaining bytes from the piped output. - * - * @return string the remaining output data - */ - public function readAll() - { - $data = ''; - while ($read = $this->read($this->maxReadBytes, false)) { - $data .= $read; - } - - return $data; - } - - /** - * Convenience method for reading a single line, with the line ending included - * in the output. - * - * @return string|boolean the next output line, or false if none available - */ - public function readLine() - { - if (!$this->command || !is_resource($this->handle) || feof($this->handle)) - return false; - - $line = fgets($this->handle, $this->maxReadBytes); - $this->offset = $this->tell(); - - return $line; - } - - /** - * Moves the current offset pointer to a position in the piped command output. - * - * Since only seeking ahead in the pipe is possible - and only by reading the - * output stream - seeking to an earlier offset necessarily means invoking the - * command again, so care must be taken that any commands are idempotent. - * - * @param integer $pos new pointer position - * @return void - * @throws InvalidArgumentException - */ - public function seek($pos) - { - if ($pos < 0) - throw new InvalidArgumentException("Could not seek to offset: {$pos}"); - - if ($this->command) { - if ($pos < $this->offset) { - $this->open($this->command); - } - if ($pos > $this->offset && $pos > 0) { - $this->read($pos - $this->offset); - } - } - - $this->offset = $this->tell(); - } - - /** - * Provides the absolute position within the current piped output. - * - * @return integer the absolute position - */ - public function tell() - { - if ($this->command && is_resource($this->handle)) - return ftell($this->handle); - - return $this->offset; - } - - /** - * Sets the maximum number of bytes to read in one operation. - * - * @param integer $bytes the max bytes to read - * @return void - */ - public function setMaxReadBytes($bytes) - { - if (is_int($bytes) && $bytes > 0) { - $this->maxReadBytes = $bytes; - } - } - - /** - * The command string to be executed for piped output. - * @var string - */ - protected $command = ''; - - /** - * Stream handle for the current pipe. - * @var resource - */ - protected $handle; - - /** - * The maximum number of bytes to read in one operation. - * @var integer - */ - protected $maxReadBytes = 1048576; - - /** - * The current position in the piped output. - * @var integer - */ - protected $offset = 0; - - /** - * Resets the instance variables. - * - * @return void - */ - protected function reset() - { - $this->close(); - $this->error = ''; - $this->exitCode = 0; - $this->command = ''; - $this->offset = 0; - } - -} // End PipeReader class diff --git a/libs/zeebinz/rarinfo/rarinfo.php b/libs/zeebinz/rarinfo/rarinfo.php deleted file mode 100755 index adb836f925..0000000000 --- a/libs/zeebinz/rarinfo/rarinfo.php +++ /dev/null @@ -1,1583 +0,0 @@ - - * - * // 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']); - * } - * } - * - * - * - * 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-2016 Hecks - * @license Modified BSD - * @version 5.7 - */ -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' => (int) ($block['method'] != self::METHOD_STORE && $block['method'] != self::R50_METHOD_STORE), - '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']); - } - } - - // Add method if not set - if (!isset($block['method'])) { - $block['method'] = self::METHOD_NORMAL; - } - } - - /** - * 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 = min($this->offset + $block['data_size'], $this->length); - 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 = min($this->offset + $block['extra_size'], $this->length); - 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 diff --git a/libs/zeebinz/rarinfo/sfvinfo.php b/libs/zeebinz/rarinfo/sfvinfo.php deleted file mode 100755 index 3e6615600a..0000000000 --- a/libs/zeebinz/rarinfo/sfvinfo.php +++ /dev/null @@ -1,167 +0,0 @@ - - * - * // Load the SFV file or data - * $sfv = new SfvInfo; - * $sfv->open('./foo.sfv'); // or $sfv->setData($data); - * if ($sfv->error) { - * echo "Error: {$sfv->error}\n"; - * exit; - * } - * - * // Process the file list - * $files = $sfv->getFileList(); - * foreach ($files as $file) { - * echo $file['name'].' - '.$file['checksum']; - * } - * - * - * - * @author Hecks - * @copyright (c) 2010-2013 Hecks - * @license Modified BSD - * @version 2.1 - */ -class SfvInfo extends ArchiveReader -{ - /** - * SFV file comments. - * @var string - */ - public $comments = ''; - - /** - * Convenience method that outputs a summary list of the SFV file records, - * useful for pretty-printing. - * - * @param boolean $basenames don't include full file paths? - * @return array file record summary - */ - public function getSummary($full=false, $basenames=false) - { - $summary = array( - 'file_name' => $this->file, - 'file_size' => $this->fileSize, - 'data_size' => $this->dataSize, - 'use_range' => "{$this->start}-{$this->end}", - 'file_count' => $this->fileCount, - ); - if ($full) { - $summary['file_list'] = $this->getFileList($basenames); - } - if ($this->error) { - $summary['error'] = $this->error; - } - - return $summary; - } - - /** - * Returns a list of file records with checksums from the source SFV file. - * - * @param boolean $basenames don't include full file paths? - * @return array list of file records, empty if none are available - */ - public function getFileList($basenames=false) - { - if ($basenames) { - $ret = array(); - foreach ($this->fileList as $item) { - $item['name'] = pathinfo($item['name'], PATHINFO_BASENAME); - $ret[] = $item; - } - return $ret; - } - - return $this->fileList; - } - - /** - * Returns the position of the archive marker/signature. - * - * @return mixed Marker position, or false if none found - */ - public function findMarker() - { - return $this->markerPosition = 0; - } - - /** - * The parsed file list with checksum info. - * @var array - */ - protected $fileList = array(); - - /** - * Parses the source data and stores a list of valid file records locally. - * - * @return boolean false if parsing fails - */ - protected function analyze() - { - // Get the available data up to the maximum allowed - try { - $data = $this->read(min($this->length, $this->maxReadBytes)); - } catch (Exception $e) { - $this->close(); - return false; - } - - // Split on all line ending types - foreach (preg_split('/\R/', $data, -1, PREG_SPLIT_NO_EMPTY) as $line) { - - // Store comment lines - if (strpos($line, ';') === 0) { - $this->comments .= trim(substr($line, 1))."\n"; - continue; - } - - if (preg_match('/^(.+?)\s+([[:xdigit:]]{2,8})$/', trim($line), $matches)) { - - // Store the file record locally - $this->fileList[] = array( - 'name' => $matches[1], - 'checksum' => $matches[2] - ); - - // Increment the filecount - $this->fileCount++; - - } else { - - // Contains invalid chars, so assume this isn't an SFV - $this->error = 'Not a valid SFV file'; - $this->close(); - return false; - } - } - - // Analysis was successful - $this->close(); - return true; - } - - /** - * Resets the instance variables before parsing new data. - * - * @return void - */ - protected function reset() - { - parent::reset(); - $this->fileList = array(); - $this->comments = ''; - } - -} // End SfvInfo class diff --git a/libs/zeebinz/rarinfo/srrinfo.php b/libs/zeebinz/rarinfo/srrinfo.php deleted file mode 100755 index d0f9259ca4..0000000000 --- a/libs/zeebinz/rarinfo/srrinfo.php +++ /dev/null @@ -1,332 +0,0 @@ - - * - * // Load the SRR file or data - * $srr = new SrrInfo; - * $srr->open('./foo.srr'); // or $srr->setData($data); - * if ($srr->error) { - * echo "Error: {$srr->error}\n"; - * exit; - * } - * - * // Get any SRR stored files - * $stored = $srr->getStoredFiles(); - * foreach ($stored as $file) { - * echo "{$file['name']} ({$file['size']}):\n"; - * echo $file['data']; - * } - * - * // Inspect the RAR file list - * $volumes = $srr->getFileList(); - * foreach ($volumes as $vol) { - * echo "Archive volume: {$vol['name']}\n"; - * foreach ($vol['files'] as $file) { - * if ($file['pass'] == true) { - * echo "-- File is passworded: {$file['name']}\n"; - * } - * } - * } - * - * - * - * @author Hecks - * @copyright (c) 2010-2013 Hecks - * @license Modified BSD - * @version 2.3 - */ -class SrrInfo extends RarInfo -{ - // ------ Class constants ----------------------------------------------------- - - /**#@+ - * SRR file format values - */ - - // SRR Block types - const SRR_BLOCK_MARK = 0x69; - const SRR_STORED_FILE = 0x6a; - const SRR_OSO_HASH = 0x6b; - const SRR_RAR_FILE = 0x71; - - // Flags for SRR Marker block - const APP_NAME_PRESENT = 0x0001; - - /**#@-*/ - - /** - * Format for unpacking any OSO hash blocks. - */ - const FORMAT_SRR_OSO_HASH = 'Vfile_size/Vfile_size_high/h16file_hash/vname_size'; - - - // ------ Instance variables and methods --------------------------------------- - - /** - * Signature for the SRR Marker block. - * @var string - */ - protected $markerBlock = "\x69\x69\x69"; - - /** - * List of block names corresponding to SRR block types. - * @var array - */ - protected $srrBlockNames = array( - self::SRR_BLOCK_MARK => 'SRR Marker', - self::SRR_STORED_FILE => 'Stored File', - self::SRR_OSO_HASH => 'OSO Hash', - self::SRR_RAR_FILE => 'RAR File', - ); - - /** - * List of block types and Subblock subtypes without bodies. - * @var array - */ - protected $headersOnly = array( - 'type' => array(self::BLOCK_FILE), - 'subtype' => array(self::SUBTYPE_RECOVERY), - ); - - /** - * Details of the client that created the file/data. - * @var string - */ - public $client = ''; - - /** - * Initializes the class instance. - * - * @return void - */ - public function __construct($file=null, $isFragment=false, array $range=null) - { - // Merge the SRR and RAR block names - $this->blockNames = $this->srrBlockNames + $this->blockNames; - - parent::__construct($file, $isFragment, $range); - } - - /** - * Convenience method that outputs a summary list of the SRR information, - * useful for pretty-printing. - * - * @param boolean $full include full info, e.g. stored file data? - * @param boolean $skipDirs should RAR directory entries be skipped? - * @return array SRR file summary - */ - public function getSummary($full=false, $skipDirs=false) - { - $summary = array( - 'file_name' => $this->file, - 'file_size' => $this->fileSize, - 'data_size' => $this->dataSize, - 'client' => $this->client, - 'stored_files' => $this->getStoredFiles($full), - ); - if ($osoInfo = $this->getOsoInfo()) { - $summary['oso_info'] = $osoInfo; - } - $fileList = $this->getFileList($skipDirs); - $summary['file_count'] = count($fileList); - if ($full) { - $summary['file_list'] = $fileList; - } - if ($this->error) { - $summary['error'] = $this->error; - } - - return $summary; - } - - /** - * Parses the stored blocks and returns a list of the Stored File records, - * optionally with the file content data included. - * - * @param boolean $extract include file data in the result? - * @return mixed false if no stored files blocks available, or array of records - */ - public function getStoredFiles($extract=true) - { - if (empty($this->blocks)) {return false;} - $ret = array(); - - foreach ($this->blocks as $block) { - if ($block['head_type'] == self::SRR_STORED_FILE) { - $b = array( - 'name' => $block['file_name'], - 'size' => (isset($block['add_size']) ? $block['add_size'] : 0), - ); - if ($extract) { - $b['data'] = $block['file_data']; - } - $ret[] = $b; - } - } - - return $ret; - } - - /** - * Parses the stored blocks and returns summary info of any OSO hash block - * in the SRR file/data. - * - * @link http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes - * - * @return array|boolean OSO info, or false if none is available - */ - public function getOsoInfo() - { - if (empty($this->blocks)) {return false;} - - foreach ($this->blocks as $block) { - if ($block['head_type'] == self::SRR_OSO_HASH) { - return array( - 'name' => $block['file_name'], - 'size' => $block['file_size'], - 'hash' => $block['file_hash'], - ); - } - } - - return false; - } - - /** - * Parses the stored blocks and returns a list of the RAR volume records that the - * SRR data covers. - * - * @param boolean $skipDirs should directory entries be skipped? - * @return array|boolean list of file records, or false if none are available - */ - public function getFileList($skipDirs=false) - { - $list = array(); - $i = -1; - - foreach ($this->blocks as $block) { - - // Start a new RAR volume record - if ($block['head_type'] == self::SRR_RAR_FILE) { - $list[++$i] = array('name' => $block['file_name']); - - // Append the file summaries to the current volume record - } elseif ($block['head_type'] == self::BLOCK_FILE) { - if ($skipDirs && !empty($block['is_dir'])) {continue;} - $list[$i]['files'][] = $this->getFileBlockSummary($block); - } - } - - return $list; - } - - // SRR files do not include any file contents - public function getFileData($filename) {return false;} - public function saveFileData($filename, $destination) {return false;} - public function extractFile($filename, $destination=null, $password=null) {return false;} - - /** - * Parses the SRR data and stores a list of valid blocks locally. - * - * @return boolean false if parsing fails - */ - protected function analyze() - { - // Find the SRR MARKER block, or abort if none is found - if (($startPos = $this->findMarker()) === false) { - $this->error = 'Could not find Marker block, not a valid SRR file'; - return false; - } - - // Start at the SRR MARKER block - $this->seek($startPos); - - // Analyze all valid blocks - while ($this->offset < $this->length) try { - - // Get the next block header - $block = $this->getNextBlock(); - - // Block type: SRR MARKER - if ($block['head_type'] == self::SRR_BLOCK_MARK) { - if ($block['head_flags'] & self::APP_NAME_PRESENT) { - $block += self::unpack('vapp_name_size', $this->read(2), false); - $block['app_name'] = $this->read($block['app_name_size']); - $this->client = $block['app_name']; - } - if ($block['head_flags'] > 1 || $this->offset != $block['next_offset']) { - $this->error = 'Could not find Marker block, not a valid SRR file'; - return false; - } - - // Block type: STORED FILE - } elseif ($block['head_type'] == self::SRR_STORED_FILE) { - $block += self::unpack('vname_size', $this->read(2), false); - $block['file_name'] = $this->read($block['name_size']); - $block['file_data'] = $this->read((isset($block['add_size']) ? $block['add_size'] : 0)); - - // Block type: OSO HASH - } elseif ($block['head_type'] == self::SRR_OSO_HASH) { - $block += self::unpack(self::FORMAT_SRR_OSO_HASH, $this->read(18)); - $block['file_hash'] = strrev($block['file_hash']); - $block['file_size'] = self::int64($block['file_size'], $block['file_size_high']); - $block['file_name'] = $this->read($block['name_size']); - - // Block type: SRR RAR FILE - } elseif ($block['head_type'] == self::SRR_RAR_FILE) { - $block += self::unpack('vname_size', $this->read(2), false); - $block['file_name'] = $this->read($block['name_size']); - - // Default to RAR block processing - } else { - parent::processBlock($block); - } - - // Add current block to the list - $this->blocks[] = $block; - - // 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; - } - - // Analysis was successful - return true; - } - - /** - * Resets the instance variables before parsing new data. - * - * @return void - */ - protected function reset() - { - parent::reset(); - $this->client = ''; - } - -} // End SrrInfo class diff --git a/libs/zeebinz/rarinfo/szipinfo.php b/libs/zeebinz/rarinfo/szipinfo.php deleted file mode 100755 index bd8834f937..0000000000 --- a/libs/zeebinz/rarinfo/szipinfo.php +++ /dev/null @@ -1,1509 +0,0 @@ - 'Start', - self::PROPERTY_HEADER => 'Header', - self::PROPERTY_ADDITIONAL_STREAMS_INFO => 'Additional Streams Info', - self::PROPERTY_MAIN_STREAMS_INFO => 'Main Streams Info', - self::PROPERTY_FILES_INFO => 'Files Info', - self::PROPERTY_ENCODED_HEADER => 'Encoded Header', - self::PROPERTY_END => 'End', - ); - - /** - * Are the archive headers encrypted? - * @var boolean - */ - public $isEncrypted = false; - - /** - * Is the archive packed as a solid stream? - * @var boolean - */ - public $isSolid = false; - - /** - * The number of packed streams in the archive. - * @var integer - */ - public $blockCount = 0; - - /** - * Convenience method that outputs a summary list of the file/data information, - * useful for pretty-printing. - * - * @param boolean $full add file list to output? - * @param boolean $skipDirs should directory entries be skipped? - * @return array file/data 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}", - 'solid_pack' => (int) $this->isSolid, - 'enc_header' => (int) $this->hasEncodedHeader, - 'is_encrypted' => (int) $this->isEncrypted, - 'num_blocks' => $this->blockCount, - ); - $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 7z headers found in the file/data in human-readable - * format (for debugging purposes only). - * - * @return array|boolean list of stored headers, or false if none available - */ - public function getHeaders() - { - if (empty($this->headers)) {return false;} - $ret = array(); - - foreach ($this->headers as $header) { - $h = array(); - $h['type_name'] = isset($this->headerNames[$header['type']]) - ? $this->headerNames[$header['type']] : 'Unknown'; - $h += $header; - $ret[] = $h; - } - - return $ret; - } - - /** - * Parses the stored headers 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) - { - // Check that headers are stored - if (!($info = $this->getFilesHeaderInfo()) || empty($info['files'])) - return array(); - - // Files may be stored in their own folders or as substreams - $streams = $this->getMainStreamsInfo(); - if (!empty($streams['substreams']['unpack_sizes'])) { - $unpackSizes = $streams['substreams']['unpack_sizes']; - } elseif (!empty($streams['folders'])) { - foreach ($streams['folders'] as $folder) { - $unpackSizes[] = $this->getFolderUnpackSize($folder); - } - } - $packRanges = $this->getPackedRanges(); - $ret = array(); - - // Collate the file & streams info - $folderIndex = $sizeIndex = $streamIndex = 0; - foreach ($info['files'] as $file) { - $item = array( - 'name' => substr($file['file_name'], 0, $this->maxFilenameLength), - 'size' => ($file['has_stream'] && isset($unpackSizes[$sizeIndex])) ? $unpackSizes[$sizeIndex] : 0, - 'date' => isset($file['utime']) ? $file['utime'] : 0, - 'pass' => 0, - 'compressed' => 0, - ); - if (!empty($file['is_dir'])) { - if ($skipDirs) {continue;} - $item['is_dir'] = 1; - } - if ($file['has_stream']) { - $numStreamsInFolder = 1; - if (!empty($streams['folders'][$folderIndex])) { - $folder = $streams['folders'][$folderIndex]; - $item['pass'] = $folder['is_encrypted']; - $item['compressed'] = $folder['is_compressed']; - $item['block'] = $folderIndex; - if (isset($streams['substreams']['num_unpack_streams'][$folderIndex])) { - $numStreamsInFolder = $streams['substreams']['num_unpack_streams'][$folderIndex]; - } - } - if ($packRanges[$folderIndex] != null) { - $item['range'] = $packRanges[$folderIndex]; - } - if (!empty($streams['substreams']['digests_defined'][$sizeIndex])) { - $item['crc32'] = dechex($streams['substreams']['digests'][$sizeIndex]); - } - if (++$streamIndex == $numStreamsInFolder) { - $streamIndex = 0; - $folderIndex++; - } - $sizeIndex++; - } - $ret[] = $item; - } - - return $ret; - } - - /** - * Retrieves the raw data for the given filename. Note that this is only useful - * if the file hasn't been compressed or encrypted. - * - * @param string $filename name of the file to retrieve - * @return mixed file data, or false if no file info available - */ - public function getFileData($filename) - { - // Check that headers are stored and data source is available - if (empty($this->headers) || ($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. Note - * that this is only useful if the file isn't compressed or encrypted. - * - * @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 headers are stored and data source is available - if (empty($this->headers) || ($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 archive password for decoding encypted headers. - * - * @param string $password the password - * @return void - */ - public function setPassword($password) - { - $this->password = $password; - } - - /** - * Sets the absolute path to the external 7za 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 7za - * 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(); - - // Set the external command - $pass = $password ? '-p'.escapeshellarg($password) : ''; - $command = '"'.$this->externalClient.'"' - ." e -so -bd -y -t7z {$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, 'Everything is Ok') === false) { - if ($destination) {@unlink($destination);} - $this->error = $error; - return false; - } - - return $destination ? $written : $data; - } - - /** - * Returns the position of the starting header signature in the file/data. - * - * @return mixed start position, or false if no valid signature found - */ - public function findMarker() - { - if ($this->markerPosition !== null) - return $this->markerPosition; - - try { - $buff = $this->read(min($this->length, $this->maxReadBytes)); - $this->rewind(); - return $this->markerPosition = strpos($buff, self::MARKER_SIGNATURE); - } catch (Exception $e) { - return false; - } - } - - /** - * List of headers found in the file/data. - * @var array - */ - protected $headers = array(); - - /** - * Are the archive headers encoded? - * @var boolean - */ - protected $hasEncodedHeader = false; - - /** - * The archive password for decoding encrypted headers. - * @var string - */ - protected $password = ''; - - /** - * Full path to the external 7za client. - * @var string - */ - protected $externalClient = ''; - - /** - * Parses the 7z data and stores a list of valid headers locally. - * - * @return boolean false if parsing fails - */ - protected function analyze() - { - // Find the marker signature, if there is one - $startPos = $this->findMarker(); - if ($startPos === false && !$this->isFragment) { - - // Not a 7z fragment or valid file, so abort here - $this->error = 'Could not find marker signature, not a valid 7z file'; - return false; - - } elseif ($startPos !== false) { - - // Unpack the Start header - $this->seek($startPos + strlen(self::MARKER_SIGNATURE)); - $header = $this->readStartHeader(); - $this->headers[] = $header; - - // Go to the next header if available - $this->seek(min($header['next_offset'], $this->length)); - - } elseif ($this->isFragment) { - - // Search for a valid header and continue unpacking from there - if (($startPos = $this->findHeader()) === false) { - $this->error = 'Could not find a valid 7z header'; - return false; - } - $this->seek($startPos); - } - - // Analyze all headers - while ($this->offset < $this->length) try { - - // Get the next header - if (($header = $this->readNextHeader()) === false) - break; - - // Add the current header to the list - $this->headers[] = $header; - - // Skip to the next header, if any - if ($this->offset != $header['next_offset']) { - $this->seek($header['next_offset']); - } - - // Sanity check - if ($header['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 headers - if (empty($this->headers)) { - $this->error = 'No valid 7z headers were found'; - return false; - } - - // Analysis was successful - if ($this->hasEncodedHeader && $this->externalClient) { - return $this->extractHeaders(); - } - return true; - } - - /** - * Searches for the position of a valid header up to maxReadBytes, and sets - * it as the start of the data to analyze. - * - * @return integer|boolean the header offset, or false if none is found - */ - protected function findHeader() - { - // 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 - $searches = array( - 'unencoded' => pack('C*', self::PROPERTY_HEADER, self::PROPERTY_MAIN_STREAMS_INFO), - 'encoded' => pack('C*', self::PROPERTY_ENCODED_HEADER, self::PROPERTY_PACK_INFO), - ); - if (!($positions = self::strposall($buffer, $searches))) - return false; - - foreach ($positions as $offset => $matches) try { - $offset += $start; - $this->seek(($matches[0] == 'encoded') ? $offset : $offset + 1); - - // Verify the Header or Encoded Header data - if (($header = $this->readNextHeader()) && $this->sanityCheckStreamsInfo($header)) { - return $this->markerPosition = $offset; - } - - // No more readable data, or read error - } catch (Exception $e) {continue;} - - return false; - } - - /** - * Unpacks the Start header info from the current offset. - * - * @return array|boolean the Start header info, or false on error - */ - protected function readStartHeader() - { - $header = array( - 'offset' => $this->offset, - 'type' => self::START_HEADER, - ); - try { - $header += self::unpack(self::START_HEADER_FORMAT, $this->read(self::START_HEADER_SIZE)); - } catch (Exception $e) { - return false; - } - $header['next_head_offset'] = self::int64($header['next_head_offset'], $header['next_head_offset_high']); - $header['next_head_size'] = self::int64($header['next_head_size'], $header['next_head_size_high']); - $header['next_offset'] = $this->offset + $header['next_head_offset']; - $header['data_offset'] = $this->offset; - - return $header; - } - - /** - * Reads the start of the next header before further processing by type. - * - * @return array|boolean the next header info, or false on error - */ - protected function readNextHeader() - { - $header = array( - 'offset' => $this->offset, - 'type' => ord($this->read(1)), - 'next_offset' => $this->length, - ); - switch ($header['type']) { - - // Start/end header markers - case self::PROPERTY_HEADER: - case self::PROPERTY_END: - $header['next_offset'] = $header['offset'] + 1; - return $header; - - case self::PROPERTY_ARCHIVE_PROPERTIES: - return $this->processArchiveProperties($header); - - case self::PROPERTY_ADDITIONAL_STREAMS_INFO: - case self::PROPERTY_MAIN_STREAMS_INFO: - case self::PROPERTY_ENCODED_HEADER: - return $this->processStreamsInfo($header); - - case self::PROPERTY_FILES_INFO: - return $this->processFilesInfo($header); - - // Unknown types - default: - return $header; - } - } - - /** - * Reads & parses info about various archive streams from the current offset, - * and adds it to the given header record. - * - * @param array $header a valid header record - * @return boolean false on error - */ - protected function processStreamsInfo(&$header) - { - $nid = ord($this->read(1)); - - // Pack Info - if ($nid == self::PROPERTY_PACK_INFO) { - if (!$this->processPackInfo($header)) - return false; - $this->isSolid = (bool) $header['is_solid']; - $nid = ord($this->read(1)); - } - - // Unpack Info - if ($nid == self::PROPERTY_UNPACK_INFO) { - if (!$this->processUnpackInfo($header)) - return false; - $this->blockCount = $header['num_folders']; - $nid = ord($this->read(1)); - } - - // Substreams Info - if ($nid == self::PROPERTY_SUBSTREAMS_INFO) { - if (!$this->processSubstreamsInfo($header)) - return false; - $nid = ord($this->read(1)); - } - - // End Streams Info - if (!$this->checkIsEnd($nid)) {return false;} - $this->hasEncodedHeader = ($header['type'] == self::PROPERTY_ENCODED_HEADER); - $header['next_offset'] = $this->offset; - - return $header; - } - - /** - * Reads & parses basic info about the packed streams in the archive. - * - * @param array $header a valid header record - * @return boolean false on error - */ - protected function processPackInfo(&$header) - { - $header['pack_offset'] = $this->readNumber(); - $header['num_streams'] = $this->readNumber(); - $header['is_solid'] = (int) ($header['num_streams'] == 1); - $nid = ord($this->read(1)); - - // Packed sizes - if ($nid == self::PROPERTY_SIZE) { - $header['pack_sizes'] = array(); - for ($i = 0; $i < $header['num_streams']; $i++) { - $header['pack_sizes'][$i] = $this->readNumber(); - } - $nid = ord($this->read(1)); - - // Packed CRC digests - if ($nid == self::PROPERTY_CRC) { - $digests = $this->readDigests($header['num_streams']); - $header['pack_digests_defined'] = array(); - $header['pack_crcs'] = array(); - for ($i = 0; $i < $header['num_streams']; $i++) { - $header['pack_digests_defined'][$i] = $digests['defined'][$i]; - $header['pack_crcs'][$i] = $digests['crcs'][$i]; - } - $nid = ord($this->read(1)); - } - } - - return $this->checkIsEnd($nid); - } - - /** - * Reads & parses further info about the contents of the packed streams. - * - * @param array $header a valid header record - * @return boolean false on error - */ - protected function processUnpackInfo(&$header) - { - $nid = ord($this->read(1)); - - // Folders (packed stream blocks) - if ($nid != self::PROPERTY_FOLDER) { - $this->error = "Expecting PROPERTY_FOLDER but found: {$nid} at: ".($this->offset - 1); - return false; - } - $header['num_folders'] = $this->readNumber(); - if (!$this->checkExternal()) {return false;} - $this->processFolders($header); - $nid = ord($this->read(1)); - - // Unpack sizes - if ($nid != self::PROPERTY_CODERS_UNPACK_SIZE) { - $this->error = "Expecting PROPERTY_CODERS_UNPACK_SIZE but found: {$nid} at: ".($this->offset - 1); - return false; - } - foreach ($header['folders'] as &$folder) { - $folder['unpack_sizes'] = array(); - for ($i = 0; $i < $folder['total_out_streams']; $i++) { - $folder['unpack_sizes'][] = $this->readNumber(); - } - } - $nid = ord($this->read(1)); - - // Unpack digests - if ($nid == self::PROPERTY_CRC) { - $digests = $this->readDigests($header['num_folders']); - for ($i = 0; $i < $header['num_folders']; $i++) { - $header['folders'][$i]['digest_defined'] = $digests['defined'][$i]; - $header['folders'][$i]['unpack_crc'] = $digests['crcs'][$i]; - } - $nid = ord($this->read(1)); - } - - return $this->checkIsEnd($nid); - } - - /** - * Reads & parses info about the packed 'folders' or stream blocks. These - * may combine data from multiple files as substreams to improve compression, - * and may each use multiple (chained) encoding methods. - * - * @param array $header a valid header record - * @return void - */ - protected function processFolders(&$header) - { - $header['folders'] = array(); - - for ($f = 0; $f < $header['num_folders']; $f++) { - $folder = array( - 'is_encrypted' => 0, - 'is_compressed' => 0, - 'num_coders' => $this->readNumber(), - ); - $totalInStreams = $totalOutStreams = 0; - - // Coders info - $folder['coders'] = array(); - for ($c = 0; $c < $folder['num_coders']; $c++) { - $coder = array(); - $coder['flags'] = ord($this->read(1)); - $codecSize = $coder['flags'] & 0x0f; - $isComplex = $coder['flags'] & 0x10; - $hasProps = $coder['flags'] & 0x20; - - // In/out streams - $coder += self::unpack('H*method', $this->read($codecSize)); - $coder['num_in_streams'] = $isComplex ? $this->readNumber() : 1; - $coder['num_out_streams'] = $isComplex ? $this->readNumber() : 1; - $totalInStreams += $coder['num_in_streams']; - $totalOutStreams += $coder['num_out_streams']; - - // Properties - if ($hasProps) { - $propSize = $this->readNumber(); - $coder['prop_size'] = $propSize; - $coder += self::unpack('H*properties', $this->read($propSize)); - } - $folder['coders'][] = $coder; - - // Encryption & compression - if ($coder['method'] == self::METHOD_7Z_AES) { - $folder['is_encrypted'] = 1; - if ($header['type'] == self::PROPERTY_ENCODED_HEADER) { - $this->isEncrypted = true; - } - } elseif ($coder['method'] != self::METHOD_COPY) { - $folder['is_compressed'] = 1; - } - } - $folder['total_out_streams'] = $totalOutStreams; - $folder['total_in_streams'] = $totalInStreams; - - // Bind pairs - $folder['num_bind_pairs'] = $numBindPairs = $totalOutStreams - 1; - $bindPairs = array(); - if ($numBindPairs > 0) { - for ($p = 0; $p < $numBindPairs; $p++) { - $bindPairs[] = array( - 'in' => $this->readNumber(), - 'out' => $this->readNumber(), - ); - } - $folder['bind_pairs'] = $bindPairs; - } - - // Packed indexes - $folder['num_packed_streams'] = $numPackedStreams = $totalInStreams - $numBindPairs; - $packedIndexes = array(); - if ($numPackedStreams == 1) { - for ($i = 0; $i < $totalInStreams; $i++) { - if ($this->findBindPair($bindPairs, $i, 'in') < 1) { - $packedIndexes[] = $i; - } - } - } elseif ($numPackedStreams > 1) { - for ($i = 0; $i < $numPackedStreams; $i++) { - $packedIndexes[] = $this->readNumber(); - } - } - $folder['packed_indexes'] = $packedIndexes; - $header['folders'][] = $folder; - } - } - - /** - * Reads & parses info about the substreams in each packed 'folder'. - * - * @param array $header a valid header record - * @return boolean false on error - */ - protected function processSubstreamsInfo(&$header) - { - if (empty($header['folders'])) { - $this->error = 'No folders found, cannot process substreams info'; - return false; - } - $nid = ord($this->read(1)); - $subs = array(); - - // Number of unpack streams in each folder - if ($nid == self::PROPERTY_NUM_UNPACK_STREAM) { - $subs['num_unpack_streams'] = array(); - for ($i = 0; $i < $header['num_folders']; $i++) { - $subs['num_unpack_streams'][] = $this->readNumber(); - } - $nid = ord($this->read(1)); - } else { - $subs['num_unpack_streams'] = array_fill(0, $header['num_folders'], 1); - } - - // Substream unpack sizes - if ($nid == self::PROPERTY_SIZE) { - $subs['unpack_sizes'] = array(); - for ($i = 0; $i < $header['num_folders']; $i++) { - $sum = 0; - if (($numStreams = $subs['num_unpack_streams'][$i]) == 0) - continue; - for ($j = 1; $j < $numStreams; $j++) { - $size = $this->readNumber(); - $subs['unpack_sizes'][] = $size; - $sum += $size; - } - $subs['unpack_sizes'][] = $this->getFolderUnpackSize($header['folders'][$i]) - $sum; - } - $nid = ord($this->read(1)); - } - - // Substream unpack digests (for streams with unknown CRC) - $numDigests = $numDigestsTotal = 0; - for ($i = 0; $i < $header['num_folders']; $i++) { - $numStreams = $subs['num_unpack_streams'][$i]; - if ($numStreams != 1 || empty($header['folders'][$i]['digest_defined'])) { - $numDigests += $numStreams; - } - $numDigestsTotal += $numStreams; - } - if ($nid == self::PROPERTY_CRC) { - $subs['digests_defined'] = array(); - $subs['digests'] = array(); - $digests = $this->readDigests($numDigests); - $digestIndex = 0; - for ($i = 0; $i < $header['num_folders']; $i++) { - $numStreams = $subs['num_unpack_streams'][$i]; - if ($numStreams == 1 && !empty($header['folders'][$i]['digest_defined'])) { - $subs['digests_defined'][] = 1; - $subs['digests'][] = $header['folders'][$i]['unpack_crc']; - } else { - for ($j = 0; $j < $numStreams; $j++, $digestIndex++) { - $subs['digests_defined'][] = $digests['defined'][$digestIndex]; - $subs['digests'][] = $digests['crcs'][$digestIndex]; - } - } - } - $nid = ord($this->read(1)); - } else { - $subs['digests_defined'] = array_fill(0, $numDigestsTotal, 0); - $subs['digests'] = array_fill(0, $numDigestsTotal, 0); - } - - $header['substreams'] = $subs; - return $this->checkIsEnd($nid); - } - - /** - * Reads & parses information about the files stored in the archive. - * - * @param array $header a valid header record - * @return boolean false on error - */ - protected function processFilesInfo(&$header) - { - // Start the file list - $header['num_files'] = $this->fileCount = $this->readNumber(); - $header['files'] = array(); - for ($i = 0; $i < $header['num_files']; $i++) { - $header['files'][$i]['has_stream'] = 1; - } - $numEmptyStreams = 0; - - // Read the file info properties - while ($this->offset < $this->length) { - - // Property type & size - $type = $this->readNumber(); - if ($type > 255) { - $this->error = "Invalid type, must be below 256: {$type} at: ".($this->offset - 1); - return false; - } elseif ($type == self::PROPERTY_END) { - break; - } - $size = $this->readNumber(); - - // File names - if ($type == self::PROPERTY_NAME) { - if (!$this->checkExternal()) {return false;} - foreach ($header['files'] as &$file) { - $name = ''; - while ($this->offset < $this->length) { - $data = $this->read(2); - if ($data == "\x00\x00") { - $file['file_name'] = @iconv('UTF-16LE', 'UTF-8//IGNORE//TRANSLIT', $name); - break; - } - $name .= $data; - } - } - } - - // File times - elseif ($type == self::PROPERTY_LAST_WRITE_TIME) { - $this->processFileTimes($header, 'mtime'); - } - elseif ($type == self::PROPERTY_CREATION_TIME) { - $this->processFileTimes($header, 'ctime'); - } - elseif ($type == self::PROPERTY_LAST_ACCESS_TIME) { - $this->processFileTimes($header, 'atime'); - } - - // File attributes - elseif ($type == self::PROPERTY_ATTRIBUTES) { - $defined = $this->readBooleans($header['num_files'], true); - if (!$this->checkExternal()) {return false;} - foreach ($header['files'] as $i => &$file) { - if ($defined[$i] == 1) { - $file += self::unpack('Vattributes', $this->read(4)); - } else { - $file['attributes'] = null; - } - } - } - - // Start positions - elseif ($type == self::PROPERTY_START_POSITION) { - $defined = $this->readBooleans($header['num_files'], true); - if (!$this->checkExternal()) {return false;} - foreach ($header['files'] as $i => &$file) { - if ($defined[$i] == 1) { - $sp = self::unpack('Vlow/Vhigh', $this->read(8)); - $file['start_pos'] = self::int64($sp['low'], $sp['high']); - } else { - $file['start_pos'] = null; - } - } - } - - // Empty streams/files flags - elseif ($type == self::PROPERTY_EMPTY_STREAM) { - $header['empty_streams'] = $this->readBooleans($header['num_files']); - $numEmptyStreams = array_sum($header['empty_streams']); - } - elseif ($type == self::PROPERTY_EMPTY_FILE) { - $header['empty_files'] = $this->readBooleans($numEmptyStreams); - } - elseif ($type == self::PROPERTY_ANTI) { - $header['anti_files'] = $this->readBooleans($numEmptyStreams); - } - - // Skip unknowns - else {$this->read($size);} - } - - // Process empty streams/files - $emptyFileIndex = 0; - foreach ($header['files'] as $i => &$file) { - if (!empty($header['empty_streams'][$i])) { - $file['has_stream'] = 0; - if (empty($header['empty_files'][$emptyFileIndex])) { - $file['is_dir'] = 1; - } - if (!empty($header['anti_files'][$emptyFileIndex])) { - $file['is_anti'] = 1; - } - $emptyFileIndex++; - } - } - - // End Files Info - $header['next_offset'] = $this->offset; - return $header; - } - - /** - * Reads & parses info about the given file time properties. - * - * @param array $header a valid header record - * @param string $type the file time type - * @return boolean false on error - */ - protected function processFileTimes(&$header, $type) - { - $defined = $this->readBooleans($header['num_files'], true); - if (!$this->checkExternal()) {return false;} - - foreach ($header['files'] as $i => &$file) { - if ($defined[$i] == 1) { - $time = self::unpack('Vlow/Vhigh', $this->read(8)); - $file[$type] = $time; - if ($type == 'mtime') { - $utime = self::win2unixtime($time['low'], $time['high']); - $file['utime'] = $utime; - } - } else { - $file[$type] = null; - } - } - } - - /** - * This doesn't seem to be implemented by any client, not even in the reference - * C++ code for the 7-zip client. - * - * @param array $header a valid header record - * @return boolean false on error - */ - protected function processArchiveProperties(&$header) - { - $this->error = 'Archive properties not implemented, at: '.$this->offset; - return false; - } - - /** - * Determines whether the given property ID byte is an end ID, otherwise sets - * an error for the related offset. - * - * @param string $nid the property ID - * @return boolean false on error - */ - protected function checkIsEnd($nid) - { - if ($nid != self::PROPERTY_END) { - $this->error = "Expecting PROPERTY_END but found: {$nid} at: ".($this->offset - 1); - return false; - } - return true; - } - - /** - * Determines whether an external switch has been set at the current offset - * and sets an error if it has, since the feature isn't supported. - * - * @return boolean false if the external switch is set - */ - protected function checkExternal() - { - $external = ord($this->read(1)); - if ($external != 0) { - $this->error = "External switch not implemented, at: ".($this->offset - 1); - return false; - } - return true; - } - - /** - * Tests whether the given header is a valid streams info header. - * - * @param array $header the header to sanity check - * @param integer $limit the minimum failure threshold - * @return boolean false if the sanity check fails - */ - protected function sanityCheckStreamsInfo($header, $limit=3) - { - $fail = (!isset($header['pack_offset']) || $header['pack_offset'] > PHP_INT_MAX) - + (empty($header['pack_sizes']) || $header['pack_sizes'][0] > PHP_INT_MAX) - + (empty($header['num_folders']) || $header['num_folders'] > 50) - + (empty($header['num_streams']) || $header['num_streams'] > 50) - + (empty($header['folders']) || empty($header['folders']['coders'])); - - return $fail < $limit; - } - - /** - * Reads a variable length integer value from the current offset, which may be - * an unsigned integer or float depending on the size and system. - * - * The first byte in the little-endian sequence contains the continuation bit - * flags, where 1 = read a new byte and add it to the value, and any remaining - * bits after 0 are the high bits of the value. The maximum value is an unsigned - * 64-bit integer in a 9-byte sequence. - * - * @return integer|float the variable length value, or zero on under/overflow - */ - protected function readNumber() - { - $first = ord($this->read(1)); - $low = $high = 0; - $mask = 0x80; - - for ($count = 0; $count < 8; $count++) { - if (($first & $mask) == 0) { - $remainder = ($first & ($mask - 1)); - if ($count < 4) { - $low += $remainder << ($count * 8); - } else { - $high += $remainder << (($count - 4) * 8); - } - if ($low < 0) {$low += 0x100000000;} - if ($high < 0) {$high += 0x100000000;} - return ($high ? self::int64($low, $high) : $low); - } - $next = ord($this->read(1)); - if ($count < 4) { - $low += $next << ($count * 8); - } else { - $high += $next << (($count - 4) * 8); - } - $mask >>= 1; - } - - return 0; - } - - /** - * Reads a list of boolean bit flags from the current offset. - * - * @param integer $count the number of booleans to read - * @param integer $checkAll read an all defined flag first? - * @return array the list of boolean flags - */ - protected function readBooleans($count, $checkAll=false) - { - if ($checkAll) { - $allDefined = ord($this->read(1)); - if ($allDefined != 0) { - return array_fill(0, $count, 1); - } - } - $result = array(); - $byte = $mask = 0; - for ($i = 0; $i < $count; $i++) { - if ($mask == 0) { - $byte = ord($this->read(1)); - $mask = 0x80; - } - $result[$i] = (int) (($byte & $mask) != 0); - $mask >>= 1; - } - - return $result; - } - - /** - * Reads a list of CRC digests from the current offset. - * - * @param integer $count the number of digests to read - * @return array the digests info - */ - protected function readDigests($count) - { - $digests = array( - 'defined' => $this->readBooleans($count, true), - 'crcs' => array(), - ); - for ($i = 0; $i < $count; $i++) { - if (!empty($digests['defined'][$i])) { - $crc = self::unpack('V', $this->read(4)); - $digests['crcs'][$i] = $crc[1]; - } else { - $digests['crcs'][$i] = 0; - } - } - - return $digests; - } - - /** - * Searches for a bind pair that corresponds with the given stream index. - * - * @param array $pairs the bind pair list - * @param integer $index the stream index to search - * @param string $type the type of bind pair ('in' or 'out') - * @return integer the bind pair index, or -1 if none found - */ - protected function findBindPair($pairs, $index, $type) - { - foreach ($pairs as $idx => $pair) { - if ($pair[$type] == $index) - return $idx; - } - return -1; - } - - /** - * Calculates the final unpack size for the given packed 'folder'. - * - * @param array $folder a valid folder record - * @return integer the final unpack size in bytes - */ - protected function getFolderUnpackSize($folder) - { - if (empty($folder['unpack_sizes'])) - return 0; - - $pairs = isset($folder['bind_pairs']) ? $folder['bind_pairs'] : array(); - for ($i = count($folder['unpack_sizes']) - 1; $i >= 0; $i--) { - if ($this->findBindPair($pairs, $i, 'out') < 0) { - return $folder['unpack_sizes'][$i]; - } - } - return 0; - } - - /** - * Calculates the absolute start and end positions for each of the packed - * blocks in the current file/data, including for sources with partial data, - * i.e. fragments or any split archive volumes. The range is set to null only - * if the packed data for the block is completely missing. - * - * @return array the list of absolute ranges - */ - protected function getPackedRanges() - { - if (!($mainStreams = $this->getMainStreamsInfo())) - return false; - - $startHeader = $this->getStartHeader(); - $mainHeader = $this->getMainHeader(); - $encodedHeader = $this->getEncodedHeader(); - $packSizes = $mainStreams['pack_sizes']; - - if ($startHeader) { - $start = $this->start + $startHeader['data_offset']; - $end = $start + array_sum($packSizes) - 1; - } else { - $start = $this->start; - if ($encodedHeader) { - $end = $start + $encodedHeader['offset'] - $encodedHeader['pack_sizes'][0] - 1; - } else { - $end = $start + $mainHeader['offset'] - 1; - } - } - - $ranges = array(); - $blockEnd = $end; - foreach (array_reverse($packSizes) as $size) { - if ($blockEnd < $start) { - $ranges[] = null; - continue; - } - $blockStart = max($start, $blockEnd - $size + 1); - $ranges[] = "{$blockStart}-{$blockEnd}"; - $blockEnd -= $size; - } - - return array_reverse($ranges); - } - - /** - * Retrieves the stored Main Streams Info header. - * - * @return array|boolean the header info, or false if none available - */ - protected function getMainStreamsInfo() - { - foreach ($this->headers as $header) { - if ($header['type'] == self::PROPERTY_MAIN_STREAMS_INFO) - return $header; - } - return false; - } - - /** - * Retrieves the stored Encoded Header info. - * - * @return array|boolean the header info, or false if none available - */ - protected function getEncodedHeader() - { - foreach ($this->headers as $header) { - if ($header['type'] == self::PROPERTY_ENCODED_HEADER) - return $header; - } - return false; - } - - /** - * Retrieves the stored main (unencoded) Header info. - * - * @return array|boolean the header info, or false if none available - */ - protected function getMainHeader() - { - foreach ($this->headers as $header) { - if ($header['type'] == self::PROPERTY_HEADER) - return $header; - } - return false; - } - - /** - * Retrieves the stored Start header info. - * - * @return array|boolean the header info, or false if none available - */ - protected function getStartHeader() - { - foreach ($this->headers as $header) { - if ($header['type'] == self::START_HEADER) - return $header; - } - return false; - } - - /** - * Retrieves the stored Files Info header. - * - * @return array|boolean the header info, or false if none available - */ - protected function getFilesHeaderInfo() - { - foreach ($this->headers as $header) { - if ($header['type'] == self::PROPERTY_FILES_INFO) - return $header; - } - return false; - } - - /** - * 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; - } - - /** - * Extracts any encoded headers by creating a dummy archive with the encoded - * header data as the file, and extracting that file with an external client. - * The uncompressed/decrypted header info is then appended to the local stored - * headers list. - * - * @return boolean true if extraction was successful - */ - protected function extractHeaders() - { - if (!$this->externalClient) { - $this->error = 'A valid external client is needed to extract headers'; - return false; - } - if ($this->isEncrypted && !$this->password) { - $this->error = 'Archive headers are encrypted, password needed'; - return false; - } - if (($encoded = $this->getEncodedHeader()) === false) - return false; - - try { - // Fetch the Encoded Header packed data - $packSize = $encoded['pack_sizes'][0]; - if ($startHeader = $this->getStartHeader()) { - $this->seek($startHeader['data_offset'] + $encoded['pack_offset']); - } else { - $pos = $encoded['offset'] - $packSize; - if ($pos < 0) { - $this->error = 'Not enough data available to decode headers'; - return false; - } - $this->seek($pos); - } - $data = $this->read($packSize); - - // Header + Main Streams Info start - $head = pack('C*', self::PROPERTY_HEADER, self::PROPERTY_MAIN_STREAMS_INFO); - $this->seek($encoded['offset'] + 1); // skip Encoded Header id - $head .= $this->read(1); // Pack Info id - $this->readNumber(); // skip pack_offset - $head .= "\x00"; // make pack_offset = 0 - - // Remainder (skipping final null) - $head .= $this->read($encoded['next_offset'] - $this->offset - 1); - - } catch (Exception $e) {return false;} - - // Substreams Info (skipping digests) - $head .= pack('C*', self::PROPERTY_SUBSTREAMS_INFO, self::PROPERTY_NUM_UNPACK_STREAM, 1, 0, 0); - - // File Info (for dummy 'header.txt'); - $head .= pack('C', self::PROPERTY_FILES_INFO); - $head .= pack('H*', '011117006800650061006400650072002e00740078007400'); - $head .= pack('H*', '0000140a010059ca701f4a7ece0115060100200000000000'); - - // Start Header - $major = $startHeader ? $startHeader['version_major'] : 0; - $minor = $startHeader ? $startHeader['version_minor'] : 3; - $start = self::MARKER_SIGNATURE; // signature - $start .= pack('C*', $major, $minor); // version info - $startData = pack('V*', $packSize, 0); // next_head_offset - $startData .= pack('V*', strlen($head), 0, crc32($head)); // next_head_size, next_head_crc - $start .= pack('V', crc32($startData)); // start head_crc - $start .= $startData; // remainder - - // Save the dummy to a temporary file - list($hash, $temp) = $this->getTempFileName($data); - $this->tempFiles[$hash] = $temp; - file_put_contents($temp, $start.$data.$head); - unset($start, $data, $head); - - // Extract the header info with a new instance - $szip = new self($temp, true); - $szip->externalClient = $this->externalClient; - if (($data = $szip->extractFile('header.txt', null, $this->password)) - && $szip->setData($data, true) - ) { - $this->headers = array_merge($this->headers, $szip->getHeaders()); - $this->isSolid = $szip->isSolid; - $this->blockCount = $szip->blockCount; - $this->fileCount = $szip->fileCount; - $this->error = ''; - return true; - } - - // Failed miserably! - $this->error = $szip->error; - return false; - } - - /** - * Resets the instance variables before parsing new data. - * - * @return void - */ - protected function reset() - { - parent::reset(); - $this->headers = array(); - $this->isEncrypted = false; - $this->hasEncodedHeader = false; - $this->isSolid = false; - $this->blockCount = 0; - } - -} // End SzipInfo class diff --git a/libs/zeebinz/rarinfo/zipinfo.php b/libs/zeebinz/rarinfo/zipinfo.php deleted file mode 100755 index 64a76c50dc..0000000000 --- a/libs/zeebinz/rarinfo/zipinfo.php +++ /dev/null @@ -1,790 +0,0 @@ - - * - * // Load the ZIP file or data - * $zip = new ZipInfo; - * $zip->open('./foo.zip'); // or $zip->setData($data); - * if ($zip->error) { - * echo "Error: {$zip->error}\n"; - * exit; - * } - * - * // Check encryption - * if ($zip->isEncrypted) { - * echo "Archive is encrypted\n"; - * exit; - * } - * - * // Process the file list - * $files = $zip->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"; - * $zip->saveFileData($file['name'], "./destination/{$file['name']}"); - * // or $data = $zip->getFileData($file['name']); - * } - * } - * - * - * - * The ZIP specification is quite bloated (particularly when it comes to extra - * fields and OS-specific info) and only a small part of it is implemented here, - * but there's lots still to explore: - * - * @link http://www.pkware.com/documents/casestudies/APPNOTE.TXT - * - * @author Hecks - * @copyright (c) 2010-2013 Hecks - * @license Modified BSD - * @version 2.1 - */ -class ZipInfo extends ArchiveReader -{ - // ------ Class constants ----------------------------------------------------- - - /**#@+ - * ZIP file format values - */ - - // Record type signatures - const RECORD_CENTRAL_FILE = 0x02014b50; - const RECORD_LOCAL_FILE = 0x04034b50; - const RECORD_SIGNATURE = 0x05054b50; - const RECORD_ENDCENTRAL = 0x06054b50; - const RECORD_Z64_ENDCENTRAL = 0x06064b50; - const RECORD_Z64_ENDCENTRAL_LOC = 0x07064b50; - const RECORD_ARCHIVE_EXTRA = 0x08064b50; - const RECORD_DATA_DESCR = 0x08074b50; - - // General purpose flags - const FILE_ENCRYPTED = 0x0001; - const FILE_DESCRIPTOR_USED = 0x0008; - const FILE_STRONG_ENCRYPTED = 0x0040; - const FILE_EFS_UTF8 = 0x0800; - const FILE_CDR_ENCRYPTED = 0x2000; - - // Extra Field IDs - const EXTRA_ZIP64 = 0x0001; - const EXTRA_NTFS = 0x000a; - const EXTRA_UNIX = 0x000d; - const EXTRA_STRONG_ENCR = 0x0017; - const EXTRA_POSZIP = 0x4690; - const EXTRA_UNIXTIME = 0x5455; - const EXTRA_IZUNIX = 0x5855; - const EXTRA_IZUNIX2 = 0x7855; - const EXTRA_IZUNIX3 = 0x7875; - const EXTRA_WZ_AES = 0x9901; - - // OS Types - const OS_FAT = 0; - const OS_AMIGA = 1; - const OS_VMS = 2; - const OS_UNIX = 3; - const OS_VM_CMS = 4; - const OS_ATARI = 5; - const OS_HPFS = 6; - const OS_MAC = 7; - const OS_Z_SYSTEM = 8; - const OS_CPM = 9; - const OS_NTFS = 10; - const OS_MVS = 11; - const OS_VSE = 12; - const OS_ACORN = 13; - const OS_VFAT = 14; - const OS_ALT_MVS = 15; - const OS_BEOS = 16; - const OS_TANDEM = 17; - const OS_OS400 = 18; - const OS_OSX = 19; - - /**#@-*/ - - /** - * Format for unpacking Local File records. - */ - const FORMAT_LOCAL_FILE = 'Cversion_need_num/Cversion_need_os/vflags/vmethod/vlast_mod_time/vlast_mod_date/Vcrc32/Vcompressed_size/Vuncompressed_size/vfile_name_length/vextra_length'; - - /** - * Format for unpacking Central File records. - */ - const FORMAT_CENTRAL_FILE = 'Cversion_made_num/Cversion_made_os/Cversion_need_num/Cversion_need_os/vflags/vmethod/vlast_mod_time/vlast_mod_date/Vcrc32/Vcompressed_size/Vuncompressed_size/vfile_name_length/vextra_length/vcomment_length/vdisk_start/vattr_int/Vattr_ext/Vrel_offset'; - - /** - * Format for unpacking End of Central Directory records. - */ - const FORMAT_ENDCENTRAL = 'vdisk_num/vstart_disk/ventries_disk/ventries_total/Vcentral_size/Vcentral_offset/vcomment_length'; - - /** - * Format for unpacking ZIP64 format End of Central Directory records. - */ - const FORMAT_Z64_ENDCENTRAL = 'Vcentral_size/Vcentral_size_high/Cversion_made_num/Cversion_made_os/Cversion_need_num/Cversion_need_os/Vdisk_num/Vstart_disk/Ventries_disk/Ventries_disk_high/Ventries_total/Ventries_total_high/Vcentral_offset/Vcentral_offset_high'; - - /** - * Format for unpacking ZIP64 format End of Central Directory Locator records. - */ - const FORMAT_Z64_ENDCENTRAL_LOC = 'Vstart_disk/Vcentral_offset/Vcentral_offset_high/Vtotal_disks'; - - /** - * Format for unpacking Data Descriptor blocks. - */ - const FORMAT_DATA_DESCR = 'Vsignature/Vcrc32/Vcompressed_size/Vuncompressed_size'; - - /** - * Format for unpacking Extra Field blocks. - */ - const FORMAT_EXTRA_FIELD = 'vheaderID/vdata_size'; - - - // ------ Instance variables and methods --------------------------------------- - - /** - * List of record names corresponding to record types. - * @var array - */ - protected $recordNames = array( - self::RECORD_CENTRAL_FILE => 'Central File', - self::RECORD_LOCAL_FILE => 'Local File', - self::RECORD_SIGNATURE => 'Digital Signature', - self::RECORD_ENDCENTRAL => 'End of Central Directory', - self::RECORD_Z64_ENDCENTRAL => 'ZIP64 End of Central Directory', - self::RECORD_Z64_ENDCENTRAL_LOC => 'ZIP64 End of Central Directory Locator', - self::RECORD_ARCHIVE_EXTRA => 'Archive Extra Data', - self::RECORD_DATA_DESCR => 'Data Descriptor', - ); - - /** - * List of Extra Field names corresponding to header IDs. - * @var array - */ - protected $extraFieldNames = array( - self::EXTRA_ZIP64 => 'Zip64', - self::EXTRA_NTFS => 'NTFS', - self::EXTRA_UNIX => 'Unix', - self::EXTRA_STRONG_ENCR => 'Strong Encryption', - self::EXTRA_POSZIP => 'POSZIP', - self::EXTRA_UNIXTIME => 'Unix Time', - self::EXTRA_IZUNIX => 'Info-ZIP (UX)', - self::EXTRA_IZUNIX2 => 'Info-ZIP (Ux)', - self::EXTRA_IZUNIX3 => 'Info-ZIP (ux)', - self::EXTRA_WZ_AES => 'AES-256 Password Encryption', - ); - - /** - * List of Host OS names by type. - * @var array - */ - protected $hostOSNames = array( - self::OS_FAT => 'MS-DOS and OS/2 (FAT)', - self::OS_AMIGA => 'Amiga', - self::OS_VMS => 'OpenVMS', - self::OS_UNIX => 'Unix', - self::OS_VM_CMS => 'VM/CMS', - self::OS_ATARI => 'Atari', - self::OS_HPFS => 'OS/2 HPFS', - self::OS_MAC => 'Macintosh', - self::OS_Z_SYSTEM => 'Z-System', - self::OS_CPM => 'CP/M', - self::OS_NTFS => 'Windows NTFS', - self::OS_MVS => 'MVS (OS/390 - Z/OS)', - self::OS_VSE => 'VSE', - self::OS_ACORN => 'Acorn Risc', - self::OS_VFAT => 'VFAT', - self::OS_ALT_MVS => 'Alternative MVS', - self::OS_BEOS => 'BEOS', - self::OS_TANDEM => 'Tandem', - self::OS_OS400 => 'OS/400', - self::OS_OSX => 'OS X (Darwin)', - ); - - /** - * Is the archive Central Directory encrypted? - * @var boolean - */ - public $isEncrypted = false; - - /** - * Convenience method that outputs a summary list of the file/data information, - * useful for pretty-printing. - * - * @param boolean $full add file list to output? - * @param boolean $skipDirs should directory entries be skipped? - * @param boolean $central should Central File records be used? - * @return array file/data summary - */ - public function getSummary($full=false, $skipDirs=false, $central=false) - { - $summary = array( - 'file_name' => $this->file, - 'file_size' => $this->fileSize, - 'data_size' => $this->dataSize, - 'use_range' => "{$this->start}-{$this->end}", - 'file_count' => $this->fileCount, - ); - if ($full) { - $summary['file_list'] = $this->getFileList($skipDirs, $central); - } - if ($this->error) { - $summary['error'] = $this->error; - } - - return $summary; - } - - /** - * Returns a list of the ZIP records found in the file/data in human-readable - * format (for debugging purposes only). - * - * @return array|boolean list of stored records, or false if none available - */ - public function getRecords() - { - // Check that records are stored - if (empty($this->records)) {return false;} - - // Build the record list - $ret = array(); - - foreach ($this->records AS $record) { - - $r = array(); - $r['type_name'] = $this->recordNames[$record['type']]; - $r += $record; - - // Sanity check filename length - if (isset($r['file_name'])) {$r['file_name'] = substr($r['file_name'], 0, $this->maxFilenameLength);} - $ret[] = $r; - } - - return $ret; - } - - /** - * Parses the stored records and returns a list of each of the file entries, - * optionally using the Central Directory File record instead of the (more - * limited) Local File record data. Valid file records include directory entries, - * but these can be skipped. - * - * @return array list of file records, empty if none are available - */ - public function getFileList($skipDirs=false, $central=false) - { - $ret = array(); - foreach ($this->records as $record) { - if (($central && $record['type'] == self::RECORD_CENTRAL_FILE) - || (!$central && $record['type'] == self::RECORD_LOCAL_FILE) - ) { - if ($skipDirs && !empty($record['is_dir'])) {continue;} - $ret[] = $this->getFileRecordSummary($record); - } - } - - return $ret; - } - - /** - * Retrieves the raw data for the given filename. Note that this is only useful - * if the file hasn't been compressed or encrypted. - * - * @param string $filename name of the file to retrieve - * @return mixed file data, or false if no file records available - */ - public function getFileData($filename) - { - // Check that records are stored and data source is available - if (empty($this->records) || ($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. Note that - * this is only useful if the file isn't compressed or encrypted. - * - * @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 records are stored and data source is available - if (empty($this->records) || ($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 7za 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 7za - * 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(); - - // Set the external command - $pass = $password ? '-p'.escapeshellarg($password) : ''; - $command = '"'.$this->externalClient.'"' - ." e -so -bd -y -tzip {$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, 'Everything is Ok') === false) { - if ($destination) {@unlink($destination);} - $this->error = $error; - return false; - } - - return $destination ? $written : $data; - } - - /** - * List of records found in the file/data. - * @var array - */ - protected $records = array(); - - /** - * Full path to the external 7za client. - * @var string - */ - protected $externalClient = ''; - - /** - * Returns a processed summary of a Local or Central File record. - * - * @param array $record a valid file record - * @return array summary information - */ - protected function getFileRecordSummary($record) - { - $ret = array( - 'name' => substr($record['file_name'], 0, $this->maxFilenameLength), - 'size' => $record['uncompressed_size'], - 'date' => self::dos2unixtime(($record['last_mod_date'] << 16) | $record['last_mod_time']), - 'pass' => isset($record['is_encrypted']) ? ((int) $record['is_encrypted']) : 0, - 'compressed' => (int) ($record['method'] > 0), - 'next_offset' => $record['next_offset'], - ); - if (!empty($record['is_dir'])) { - $ret['is_dir'] = 1; - } elseif ($record['type'] == self::RECORD_LOCAL_FILE) { - $start = $this->start + $record['offset'] + 30 + $record['file_name_length'] + $record['extra_length']; - $end = min($this->end, $start + $record['uncompressed_size'] - 1); - $ret['range'] = "{$start}-{$end}"; - } - if (!empty($record['crc32'])) { - $ret['crc32'] = dechex($record['crc32']); - } - - 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; - } - - /** - * Returns the position of the starting record signature in the file/data. - * An 'empty' ZIP file consists only of an End of Central Directory record. - * - * @return mixed start position, or false if no valid signature found - */ - public function findMarker() - { - if ($this->markerPosition !== null) - return $this->markerPosition; - - // Buffer the data to search - try { - $buff = $this->read(min($this->length, $this->maxReadBytes)); - $this->rewind(); - } catch (Exception $e) { - return false; - } - - // Try to find the first Local File or Central File record - if (($pos = strpos($buff, pack('V', self::RECORD_LOCAL_FILE))) !== false - || ($pos = strpos($buff, pack('V', self::RECORD_CENTRAL_FILE))) !== false - ) { - return $this->markerPosition = $pos; - } - - // Otherwise this could be an empty ZIP file - return $this->markerPosition = strpos($buff, pack('V', self::RECORD_ENDCENTRAL)); - } - - /** - * Parses the ZIP data and stores a list of valid records locally. - * - * @return boolean false if parsing fails - */ - protected function analyze() - { - // Find the first record signature, if there is one - if (($startPos = $this->findMarker()) === false) { - $this->error = 'Could not find any records, not a valid ZIP file'; - return false; - } - $this->seek($startPos); - - // Analyze all records - while ($this->offset < $this->length) try { - - // Get the next record header - if (($record = $this->getNextRecord()) === false) - continue; - - // Process the current record by type - $this->processRecord($record); - - // Add the current record to the list - $this->records[] = $record; - - // Skip to the next record, if any - if ($this->offset != $record['next_offset']) { - $this->seek($record['next_offset']); - } - - // Sanity check - if ($record['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 records - if (empty($this->records)) { - $this->error = 'No valid ZIP records were found'; - return false; - } - - // Analysis was successful - return true; - } - - /** - * Reads the start of the next record header and checks the header signature - * before further processing by record type. - * - * @return mixed the next record info, or false on invalid signature - */ - protected function getNextRecord() - { - // Start the record info - $record = array('offset' => $this->offset); - - // Unpack the record signature - $record += self::unpack('Vtype', $this->read(4)); - - // Check that the record signature is valid - if (!isset($this->recordNames[$record['type']])) { - $this->seek($this->offset - 3); - return false; - } - - // Return the record info - return $record; - } - - /** - * Processes a record passed by reference based on its type. We start with just - * the header signature, and unpack the rest of each header/body from there. - * - * @param array $record the record to process - * @return void - */ - protected function processRecord(&$record) - { - // Record type: LOCAL FILE - if ($record['type'] == self::RECORD_LOCAL_FILE) { - $record += self::unpack(self::FORMAT_LOCAL_FILE, $this->read(26)); - $record['file_name'] = $this->read($record['file_name_length']); - if ($record['extra_length'] > 0) { - $this->processExtraFields($record); - } - $record['next_offset'] = $this->offset + $record['compressed_size']; - $this->fileCount++; - - // Data Descriptor follows file data? - if ($record['flags'] & self::FILE_DESCRIPTOR_USED) { - $this->seek($record['next_offset']); - $descr = self::unpack(self::FORMAT_DATA_DESCR, $this->read(16)); - $record['has_descriptor'] = true; - $record['crc32'] = $descr['crc32']; - $record['compressed_size'] = $descr['compressed_size']; - $record['uncompressed_size'] = $descr['uncompressed_size']; - $record['next_offset'] = $this->offset; - } - } - - // Record type: CENTRAL FILE - elseif ($record['type'] == self::RECORD_CENTRAL_FILE) { - $record += self::unpack(self::FORMAT_CENTRAL_FILE, $this->read(42)); - $record['file_name'] = $this->read($record['file_name_length']); - if ($record['extra_length'] > 0) { - $this->processExtraFields($record); - } - if ($record['comment_length'] > 0) { - $record['comment'] = $this->read($record['comment_length']); - } - $record['next_offset'] = $this->offset; - } - - // Record type: END OF CENTRAL DIRECTORY - elseif ($record['type'] == self::RECORD_ENDCENTRAL) { - $record += self::unpack(self::FORMAT_ENDCENTRAL, $this->read(18)); - if ($record['comment_length'] > 0) { - $record['comment'] = $this->read($record['comment_length']); - } - $record['next_offset'] = $this->offset; - $this->fileCount = $record['entries_disk']; - } - - // Record type: ZIP64 END OF CENTRAL DIRECTORY - elseif ($record['type'] == self::RECORD_Z64_ENDCENTRAL) { - $record += self::unpack(self::FORMAT_Z64_ENDCENTRAL, $this->read(50)); - $record['next_offset'] = $record['offset'] + self::int64($record['central_size'], $record['central_size_high']); - $this->fileCount = self::int64($record['entries_disk'], $record['entries_disk_high']); - } - - // Record type: ZIP64 END OF CENTRAL DIRECTORY LOCATOR - elseif ($record['type'] == self::RECORD_Z64_ENDCENTRAL_LOC) { - $record += self::unpack(self::FORMAT_Z64_ENDCENTRAL_LOC, $this->read(16)); - $record['next_offset'] = $this->offset; - } - - // Skip everything else - else { - $record['next_offset'] = $this->offset + 1; - } - - // Process any version numbers (-> major.minor) - if (isset($record['version_made_num'])) { - $num = $record['version_made_num']; - $record['made_version'] = floor($num / 10).'.'.($num % 10); - } - if (isset($record['version_need_num'])) { - $num = $record['version_need_num']; - $record['need_version'] = floor($num / 10).'.'.($num % 10); - } - - // Process Host OS info - if (isset($record['version_made_os'])) { - $os = $record['version_made_os']; - $record['made_host_os'] = isset($this->hostOSNames[$os]) ? $this->hostOSNames[$os] : 'Unknown'; - } - if (isset($record['version_need_os'])) { - $os = $record['version_need_os']; - $record['need_host_os'] = isset($this->hostOSNames[$os]) ? $this->hostOSNames[$os] : 'Unknown'; - } - - if ($record['type'] == self::RECORD_LOCAL_FILE || $record['type'] == self::RECORD_CENTRAL_FILE) { - - // Is the file encrypted? - if ($record['flags'] & self::FILE_ENCRYPTED) { - $record['is_encrypted'] = true; - } - - // Is the Central Directory encrypted (masking Local File values)? - if ($record['flags'] & self::FILE_CDR_ENCRYPTED) { - $this->isEncrypted = true; - } - - // Is this a directory entry? (quick check) - if ($record['file_name'][$record['file_name_length'] - 1] == '/') { - $record['is_dir'] = true; - } - - // Is UTF8 encoding used? - if ($record['flags'] & self::FILE_EFS_UTF8) { - $record['is_utf8'] = true; - } - } - } - - /** - * Processes Extra Field blocks for the current record. - * - * @param array $record the current record to process - * @return void - */ - protected function processExtraFields(&$record) - { - $end = ($this->offset < $this->length) ? $this->offset + $record['extra_length'] : $this->length; - while ($this->offset < $end) - { - $field = array('type_name' => ''); - $field += self::unpack(self::FORMAT_EXTRA_FIELD, $this->read(4)); - $field['type_name'] = isset($this->extraFieldNames[$field['headerID']]) ? $this->extraFieldNames[$field['headerID']] : 'Unknown'; - - // Field: ZIP64 format - if ($field['headerID'] == self::EXTRA_ZIP64) { - - // Values are only included if the record values are set to 0xFFFFFFFF or 0xFFFF - if ($record['uncompressed_size'] == 0xFFFFFFFF) { - $field += self::unpack('Vuncompressed_size/Vuncompressed_size_high', $this->read(8)); - $record['uncompressed_size'] = self::int64($field['uncompressed_size'], $field['uncompressed_size_high']); - } - if ($record['compressed_size'] == 0xFFFFFFFF) { - $field += self::unpack('Vcompressed_size/Vcompressed_size_high', $this->read(8)); - $record['compressed_size'] = self::int64($field['compressed_size'], $field['compressed_size_high']); - } - if (isset($record['rel_offset']) && $record['rel_offset'] == 0xFFFFFFFF) { - $field += self::unpack('Vrel_offset/Vrel_offset_high', $this->read(8)); - $record['rel_offset'] = self::int64($field['rel_offset'], $field['rel_offset_high']); - } - if (isset($record['disk_start']) && $record['disk_start'] == 0xFFFF) { - $field += self::unpack('Vdisk_start', $this->read(4)); - $record['disk_start'] = $field['disk_start']; - } - } - - // Field: UNIXTIME - elseif ($field['headerID'] == self::EXTRA_UNIXTIME) { - // Probably could do something with this - $this->read($field['data_size']); - - // Default: skip field - } else { - $this->read($field['data_size']); - } - - // Add the extra field info to the record - $field['end_offset'] = $this->offset; - $record['extra_fields'][] = $field; - } - } - - - /** - * Resets the instance variables before parsing new data. - * - * @return void - */ - protected function reset() - { - parent::reset(); - - $this->records = array(); - $this->isEncrypted = false; - } - -} // End ZipInfo class