mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-31 10:18:55 +00:00
455 lines
13 KiB
PHP
455 lines
13 KiB
PHP
<?php
|
|
|
|
require_once dirname(__FILE__) . '/archivereader.php';
|
|
|
|
/**
|
|
* Par2Info class.
|
|
*
|
|
* A simple class for inspecting PAR2 file data and listing information about the
|
|
* recovery set in pure PHP. Data can be streamed from a file or loaded directly
|
|
* from memory. The redundancy of the format means that details of the recovery
|
|
* set are repeated across multiple files, so inspecting any of them will normally
|
|
* produce the same results (apart from the recovery blocks themselves).
|
|
*
|
|
* Example usage:
|
|
*
|
|
* <code>
|
|
*
|
|
* // 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";
|
|
* }
|
|
* }
|
|
*
|
|
* </code>
|
|
*
|
|
* @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
|