1432 lines
40 KiB
PHP
1432 lines
40 KiB
PHP
<?php
|
|
/* vim: set noexpandtab tabstop=2 softtabstop=2 shiftwidth=2: */
|
|
|
|
/**
|
|
* GBXDataFetcher - Fetch GBX challenge/map/replay/pack data for TrackMania (TM)
|
|
* & ManiaPlanet (MP) files
|
|
* Created by Xymph <tm@gamers.org>
|
|
* Thanks to Electron for additional input & prototyping
|
|
* Based on information at http://en.tm-wiki.org/wiki/GBX,
|
|
* http://www.tm-forum.com/viewtopic.php?p=192817#p192817
|
|
* and http://en.tm-wiki.org/wiki/PAK#Header_versions_8.2B
|
|
*
|
|
* v2.5: Add lookback string Valley; strip optional digits from mood; fix empty
|
|
* thumbnail warning
|
|
* v2.4: Update GBXChallMapFetcher & GBXPackFetcher version-dependent processing;
|
|
* update lookback strings
|
|
* v2.3: Add UTF-8 decoding of parsed XML chunk's elements; strip UTF-8 BOM
|
|
* from various string fields
|
|
* v2.2: Add class GBXPackFetcher; limit loadGBXdata() to first 256KB
|
|
* v2.1: Add exception codes; add $thumbLen to GBXChallMapFetcher
|
|
* v2.0: Complete rewrite
|
|
*/
|
|
|
|
/**
|
|
* @class GBXBaseFetcher
|
|
* @brief The base GBX class with all common functionality
|
|
*/
|
|
class GBXBaseFetcher
|
|
{
|
|
public $parseXml, $xml, $xmlParsed;
|
|
|
|
public $authorVer, $authorLogin, $authorNick, $authorZone, $authorEInfo;
|
|
|
|
private $_gbxdata, $_gbxlen, $_gbxptr, $_debug, $_error, $_endianess,
|
|
$_lookbacks, $_parsestack;
|
|
|
|
// supported class ID's
|
|
const GBX_CHALLENGE_TMF = 0x03043000;
|
|
const GBX_AUTOSAVE_TMF = 0x03093000;
|
|
const GBX_CHALLENGE_TM = 0x24003000;
|
|
const GBX_AUTOSAVE_TM = 0x2403F000;
|
|
const GBX_REPLAY_TM = 0x2407E000;
|
|
|
|
const MACHINE_ENDIAN_ORDER = 0;
|
|
const LITTLE_ENDIAN_ORDER = 1;
|
|
const BIG_ENDIAN_ORDER = 2;
|
|
|
|
const LOAD_LIMIT = 256; // KBs to read in loadGBXdata()
|
|
|
|
// initialise new instance
|
|
public function __construct()
|
|
{
|
|
$this->parseXml = false;
|
|
$this->xml = '';
|
|
$this->xmlParsed = array();
|
|
$this->_debug = false;
|
|
$this->_error = '';
|
|
|
|
list($endiantest) = array_values(unpack('L1L', pack('V', 1)));
|
|
if ($endiantest != 1)
|
|
$this->_endianess = self::BIG_ENDIAN_ORDER;
|
|
else
|
|
$this->_endianess = self::LITTLE_ENDIAN_ORDER;
|
|
|
|
$this->clearGBXdata();
|
|
$this->clearLookbacks();
|
|
$this->_parsestack = array();
|
|
|
|
$this->authorVer = 0;
|
|
$this->authorLogin = '';
|
|
$this->authorNick = '';
|
|
$this->authorZone = '';
|
|
$this->authorEInfo = '';
|
|
}
|
|
|
|
// enable debug logging
|
|
protected function enableDebug()
|
|
{
|
|
$this->_debug = true;
|
|
}
|
|
|
|
// disable debug logging
|
|
protected function disableDebug()
|
|
{
|
|
$this->_debug = false;
|
|
}
|
|
|
|
// print message to stderr if debugging
|
|
protected function debugLog($msg)
|
|
{
|
|
if ($this->_debug)
|
|
fwrite(STDERR, $msg."\n");
|
|
}
|
|
|
|
// set error message prefix
|
|
protected function setError($prefix)
|
|
{
|
|
$this->_error = (string)$prefix;
|
|
}
|
|
|
|
// exit with error exception
|
|
protected function errorOut($msg, $code = 0)
|
|
{
|
|
$this->clearGBXdata();
|
|
throw new Exception($this->_error . $msg, $code);
|
|
}
|
|
|
|
// read in raw GBX data
|
|
protected function loadGBXdata($filename)
|
|
{
|
|
$gbxdata = @file_get_contents($filename, false, null, 0, self::LOAD_LIMIT * 1024);
|
|
if ($gbxdata !== false)
|
|
$this->storeGBXdata($gbxdata);
|
|
else
|
|
$this->errorOut('Unable to read GBX data from ' . $filename, 1);
|
|
}
|
|
|
|
// store raw GBX data
|
|
protected function storeGBXdata($gbxdata)
|
|
{
|
|
$this->_gbxdata = & $gbxdata;
|
|
$this->_gbxlen = strlen($gbxdata);
|
|
$this->_gbxptr = 0;
|
|
}
|
|
|
|
// clear GBX data (to avoid print_r problems & reduce memory usage)
|
|
protected function clearGBXdata()
|
|
{
|
|
$this->storeGBXdata('');
|
|
}
|
|
|
|
// get GBX pointer
|
|
protected function getGBXptr()
|
|
{
|
|
return $this->_gbxptr;
|
|
}
|
|
|
|
// set GBX pointer
|
|
protected function setGBXptr($ptr)
|
|
{
|
|
$this->_gbxptr = (int)$ptr;
|
|
}
|
|
|
|
// move GBX pointer
|
|
protected function moveGBXptr($len)
|
|
{
|
|
$this->_gbxptr += (int)$len;
|
|
}
|
|
|
|
// read $len bytes from GBX data
|
|
protected function readData($len)
|
|
{
|
|
if ($this->_gbxptr + $len > $this->_gbxlen)
|
|
$this->errorOut(sprintf('Insufficient data for %d bytes at pos 0x%04X',
|
|
$len, $this->_gbxptr), 2);
|
|
$data = '';
|
|
while ($len-- > 0)
|
|
$data .= $this->_gbxdata[$this->_gbxptr++];
|
|
return $data;
|
|
}
|
|
|
|
// read signed byte from GBX data
|
|
protected function readInt8()
|
|
{
|
|
$data = $this->readData(1);
|
|
list(, $int8) = unpack('c*', $data);
|
|
return $int8;
|
|
}
|
|
|
|
// read signed short from GBX data
|
|
protected function readInt16()
|
|
{
|
|
$data = $this->readData(2);
|
|
if ($this->_endianess == self::BIG_ENDIAN_ORDER)
|
|
$data = strrev($data);
|
|
list(, $int16) = unpack('s*', $data);
|
|
return $int16;
|
|
}
|
|
|
|
// read signed long from GBX data
|
|
protected function readInt32()
|
|
{
|
|
$data = $this->readData(4);
|
|
if ($this->_endianess == self::BIG_ENDIAN_ORDER)
|
|
$data = strrev($data);
|
|
list(, $int32) = unpack('l*', $data);
|
|
return $int32;
|
|
}
|
|
|
|
// read string from GBX data
|
|
protected function readString()
|
|
{
|
|
$gbxptr = $this->getGBXptr();
|
|
$len = $this->readInt32();
|
|
$len &= 0x7FFFFFFF;
|
|
if ($len <= 0 || $len >= 0x12000) { // for large XML & Data blocks
|
|
if ($len != 0)
|
|
$this->errorOut(sprintf('Invalid string length %d (0x%04X) at pos 0x%04X',
|
|
$len, $len, $gbxptr), 3);
|
|
}
|
|
$data = $this->readData($len);
|
|
return $data;
|
|
}
|
|
|
|
// strip UTF-8 BOM from string
|
|
protected function stripBOM($str)
|
|
{
|
|
return str_replace("\xEF\xBB\xBF", '', $str);
|
|
}
|
|
|
|
// clear lookback strings
|
|
protected function clearLookbacks()
|
|
{
|
|
$this->_lookbacks = array();
|
|
}
|
|
|
|
// read lookback string from GBX data
|
|
protected function readLookbackString()
|
|
{
|
|
if (empty($this->_lookbacks)) {
|
|
$version = $this->readInt32();
|
|
if ($version != 3)
|
|
$this->errorOut('Unknown lookback strings version: ' . $version, 4);
|
|
}
|
|
|
|
// check index
|
|
$index = $this->readInt32();
|
|
if ($index == -1) {
|
|
// unassigned (empty) string
|
|
$str = '';
|
|
} elseif (($index & 0xC0000000) == 0) {
|
|
// use external reference string
|
|
switch ($index) {
|
|
case 11: $str = 'Valley';
|
|
break;
|
|
case 12: $str = 'Canyon';
|
|
break;
|
|
case 17: $str = 'TMCommon';
|
|
break;
|
|
case 202: $str = 'Storm';
|
|
break;
|
|
case 299: $str = 'SMCommon';
|
|
break;
|
|
case 10003: $str = 'Common';
|
|
break;
|
|
default: $str = 'UNKNOWN';
|
|
}
|
|
} elseif (($index & 0x3FFFFFFF) == 0) {
|
|
// read string & add to lookbacks
|
|
$str = $this->readString();
|
|
$this->_lookbacks[] = $str;
|
|
} else {
|
|
// use string from lookbacks
|
|
$str = $this->_lookbacks[($index & 0x3FFFFFFF) - 1];
|
|
}
|
|
|
|
return $str;
|
|
}
|
|
|
|
// XML parser functions
|
|
private function startTag($parser, $name, $attribs)
|
|
{
|
|
foreach ($attribs as $key => &$val)
|
|
$val = utf8_decode($val);
|
|
//echo 'startTag: ' . $name . "\n"; print_r($attribs);
|
|
array_push($this->_parsestack, $name);
|
|
if ($name == 'DEP') {
|
|
$this->xmlParsed['DEPS'][] = $attribs;
|
|
} elseif (count($this->_parsestack) <= 2) {
|
|
// HEADER, IDENT, DESC, TIMES, CHALLENGE/MAP, DEPS, CHECKPOINTS
|
|
$this->xmlParsed[$name] = $attribs;
|
|
}
|
|
}
|
|
|
|
private function charData($parser, $data)
|
|
{
|
|
//echo 'charData: ' . $data . "\n";
|
|
if (count($this->_parsestack) == 3)
|
|
$this->xmlParsed[$this->_parsestack[1]][$this->_parsestack[2]] = $data;
|
|
elseif (count($this->_parsestack) > 3)
|
|
$this->debugLog('XML chunk nested too deeply: ', print_r($this->_parsestack, true));
|
|
}
|
|
|
|
private function endTag($parser, $name)
|
|
{
|
|
//echo 'endTag: ' . $name . "\n";
|
|
array_pop($this->_parsestack);
|
|
}
|
|
|
|
protected function parseXMLstring()
|
|
{
|
|
// define a dedicated parser to handle the attributes
|
|
$xml_parser = xml_parser_create();
|
|
xml_set_object($xml_parser, $this);
|
|
xml_set_element_handler($xml_parser, 'startTag', 'endTag');
|
|
xml_set_character_data_handler($xml_parser, 'charData');
|
|
|
|
// escape '&' characters unless already a known entity
|
|
$xml = preg_replace('/&(?!(?:amp|quot|apos|lt|gt);)/', '&', $this->xml);
|
|
|
|
if (!xml_parse($xml_parser, utf8_encode($xml), true))
|
|
$this->errorOut(sprintf('XML chunk parse error: %s at line %d',
|
|
xml_error_string(xml_get_error_code($xml_parser)),
|
|
xml_get_current_line_number($xml_parser)), 12);
|
|
|
|
xml_parser_free($xml_parser);
|
|
}
|
|
|
|
/**
|
|
* Check GBX header, main class ID & header block
|
|
* @param Array $classes
|
|
* The main class IDs accepted for this GBX
|
|
* @return Size of GBX header block
|
|
*/
|
|
protected function checkHeader(array $classes)
|
|
{
|
|
// check magic header
|
|
$data = $this->readData(3);
|
|
$version = $this->readInt16();
|
|
if ($data != 'GBX' || $version != 6)
|
|
$this->errorOut('No magic GBX header', 5);
|
|
|
|
// check header block (un)compression
|
|
$data = $this->readData(4);
|
|
if ($data[1] != 'U')
|
|
$this->errorOut('Compressed GBX header block not supported', 6);
|
|
|
|
// check main class ID
|
|
$mainClass = $this->readInt32();
|
|
if (!in_array($mainClass, $classes))
|
|
$this->errorOut(sprintf('Main class ID %08X not supported', $mainClass), 7);
|
|
$this->debugLog(sprintf('GBX main class ID: %08X', $mainClass));
|
|
|
|
// get header size
|
|
$headerSize = $this->readInt32();
|
|
if ($headerSize == 0)
|
|
$this->errorOut('No GBX header block', 8);
|
|
|
|
$this->debugLog(sprintf('GBX header block size: %d (%.1f KB)',
|
|
$headerSize, $headerSize / 1024));
|
|
return $headerSize;
|
|
} // checkHeader
|
|
|
|
/**
|
|
* Get list of chunks from GBX header block
|
|
* @param Int $headerSize
|
|
* Size of header block (chunks list & chunks data)
|
|
* @param array $chunks
|
|
* List of chunk IDs & names
|
|
* @return List of chunk offsets & sizes
|
|
*/
|
|
protected function getChunksList($headerSize, array $chunks)
|
|
{
|
|
// get number of chunks
|
|
$numChunks = $this->readInt32();
|
|
if ($numChunks == 0)
|
|
$this->errorOut('No GBX header chunks', 9);
|
|
|
|
$this->debugLog('GBX number of header chunks: ' . $numChunks);
|
|
$chunkStart = $this->getGBXptr();
|
|
$this->debugLog(sprintf('GBX start of chunk list: 0x%04X', $chunkStart));
|
|
$chunkOffset = $chunkStart + $numChunks * 8;
|
|
|
|
// get list of all chunks
|
|
$chunksList = array();
|
|
for ($i = 0; $i < $numChunks; $i++)
|
|
{
|
|
$chunkId = $this->readInt32();
|
|
$chunkSize = $this->readInt32();
|
|
|
|
$chunkId &= 0x00000FFF;
|
|
$chunkSize &= 0x7FFFFFFF;
|
|
|
|
if (array_key_exists($chunkId, $chunks)) {
|
|
$name = $chunks[$chunkId];
|
|
$chunksList[$name] = array(
|
|
'off' => $chunkOffset,
|
|
'size' => $chunkSize
|
|
);
|
|
} else {
|
|
$name = 'UNKNOWN';
|
|
}
|
|
$this->debugLog(sprintf('GBX chunk %2d %-8s Id 0x%03X Offset 0x%06X Size %6d',
|
|
$i, $name, $chunkId, $chunkOffset, $chunkSize));
|
|
$chunkOffset += $chunkSize;
|
|
}
|
|
|
|
//$this->debugLog(print_r($chunksList, true));
|
|
$totalSize = $chunkOffset - $chunkStart + 4; // numChunks
|
|
if ($headerSize != $totalSize)
|
|
$this->errorOut(sprintf('Chunk list size mismatch: %d <> %d',
|
|
$headerSize, $totalSize), 10);
|
|
|
|
return $chunksList;
|
|
} // getChunksList
|
|
|
|
/**
|
|
* Initialize for a new chunk
|
|
* @param int $offset
|
|
*/
|
|
protected function initChunk($offset)
|
|
{
|
|
$this->setGBXptr($offset);
|
|
$this->clearLookbacks();
|
|
}
|
|
|
|
/**
|
|
* Get XML chunk from GBX header block & optionally parse it
|
|
* @param array $chunksList
|
|
* List of chunk offsets & sizes
|
|
*/
|
|
protected function getXMLChunk(array $chunksList)
|
|
{
|
|
if (!isset($chunksList['XML'])) return;
|
|
|
|
$this->initChunk($chunksList['XML']['off']);
|
|
$this->xml = $this->readString();
|
|
|
|
if ($chunksList['XML']['size'] != strlen($this->xml) + 4)
|
|
$this->errorOut(sprintf('XML chunk size mismatch: %d <> %d',
|
|
$chunksList['XML']['size'], strlen($this->xml) + 4), 11);
|
|
|
|
if ($this->parseXml && $this->xml != '')
|
|
$this->parseXMLstring();
|
|
} // getXMLChunk
|
|
|
|
/**
|
|
* Get Author fields from GBX header block
|
|
*/
|
|
protected function getAuthorFields()
|
|
{
|
|
$this->authorVer = $this->readInt32();
|
|
$this->authorLogin = $this->readString();
|
|
$this->authorNick = $this->stripBOM($this->readString());
|
|
$this->authorZone = $this->stripBOM($this->readString());
|
|
$this->authorEInfo = $this->readString();
|
|
} // getAuthorFields
|
|
|
|
/**
|
|
* Get Author chunk from GBX header block
|
|
* @param array $chunksList
|
|
* List of chunk offsets & sizes
|
|
*/
|
|
protected function getAuthorChunk(array $chunksList)
|
|
{
|
|
if (!isset($chunksList['Author'])) return;
|
|
|
|
$this->initChunk($chunksList['Author']['off']);
|
|
$version = $this->readInt32();
|
|
$this->debugLog('GBX Author chunk version: ' . $version);
|
|
|
|
$this->getAuthorFields();
|
|
} // getAuthorChunk
|
|
|
|
} // class GBXBaseFetcher
|
|
|
|
|
|
/**
|
|
* @class GBXChallMapFetcher
|
|
* @brief The class that fetches all GBX challenge/map info
|
|
*/
|
|
class GBXChallMapFetcher extends GBXBaseFetcher
|
|
{
|
|
public $tnImage;
|
|
|
|
public $headerVersn, $bronzeTime, $silverTime, $goldTime, $authorTime,
|
|
$cost, $multiLap, $type, $typeName, $authorScore, $simpleEdit,
|
|
$nbChecks, $nbLaps;
|
|
public $uid, $envir, $author, $name, $kind, $kindName, $password,
|
|
$mood, $envirBg, $authorBg, $mapType, $mapStyle, $lightmap, $titleUid;
|
|
public $xmlVer, $exeVer, $exeBld, $validated, $songFile, $songUrl,
|
|
$modName, $modFile, $modUrl;
|
|
public $thumbLen, $thumbnail, $comment;
|
|
|
|
const IMAGE_FLIP_HORIZONTAL = 1;
|
|
const IMAGE_FLIP_VERTICAL = 2;
|
|
const IMAGE_FLIP_BOTH = 3;
|
|
|
|
/**
|
|
* Mirror (flip) an image across horizontal, vertical or both axis
|
|
* Source: http://www.php.net/manual/en/function.imagecopy.php#85992
|
|
* @param String $imgsrc
|
|
* Source image data
|
|
* @param Int $dir
|
|
* Flip direction from the constants above
|
|
* @return Flipped image data if successful, otherwise source image data
|
|
*/
|
|
private function imageFlip($imgsrc, $dir)
|
|
{
|
|
$width = imagesx($imgsrc);
|
|
$height = imagesy($imgsrc);
|
|
$src_x = 0;
|
|
$src_y = 0;
|
|
$src_width = $width;
|
|
$src_height = $height;
|
|
|
|
switch ((int)$dir) {
|
|
case self::IMAGE_FLIP_HORIZONTAL:
|
|
$src_y = $height;
|
|
$src_height = -$height;
|
|
break;
|
|
case self::IMAGE_FLIP_VERTICAL:
|
|
$src_x = $width;
|
|
$src_width = -$width;
|
|
break;
|
|
case self::IMAGE_FLIP_BOTH:
|
|
$src_x = $width;
|
|
$src_y = $height;
|
|
$src_width = -$width;
|
|
$src_height = -$height;
|
|
break;
|
|
default:
|
|
return $imgsrc;
|
|
}
|
|
|
|
$imgdest = imagecreatetruecolor($width, $height);
|
|
if (imagecopyresampled($imgdest, $imgsrc, 0, 0, $src_x, $src_y,
|
|
$width, $height, $src_width, $src_height)) {
|
|
return $imgdest;
|
|
}
|
|
return $imgsrc;
|
|
} // imageFlip
|
|
|
|
/**
|
|
* Instantiate GBX challenge/map fetcher
|
|
*
|
|
* @param Boolean $parsexml
|
|
* If true, the fetcher also parses the XML block
|
|
* @param Boolean $tnimage
|
|
* If true, the fetcher also extracts the thumbnail image;
|
|
* if GD/JPEG libraries are present, image will be flipped upright,
|
|
* otherwise it will be in the original upside-down format
|
|
* Warning: this is binary data in JPEG format, 256x256 pixels for
|
|
* TMU/TMF or 512x512 pixels for MP
|
|
* @param Boolean $debug
|
|
* If true, the fetcher prints debug logging to stderr
|
|
* @return GBXChallMapFetcher
|
|
* If GBX data couldn't be extracted, an Exception is thrown with
|
|
* the error message & code
|
|
*/
|
|
public function __construct($parsexml = false, $tnimage = false, $debug = false)
|
|
{
|
|
parent::__construct();
|
|
|
|
$this->headerVersn = 0;
|
|
$this->bronzeTime = 0;
|
|
$this->silverTime = 0;
|
|
$this->goldTime = 0;
|
|
$this->authorTime = 0;
|
|
|
|
$this->cost = 0;
|
|
$this->multiLap = false;
|
|
$this->type = 0;
|
|
$this->typeName = '';
|
|
|
|
$this->authorScore = 0;
|
|
$this->simpleEdit = false;
|
|
$this->nbChecks = 0;
|
|
$this->nbLaps = 0;
|
|
|
|
$this->uid = '';
|
|
$this->envir = '';
|
|
$this->author = '';
|
|
$this->name = '';
|
|
$this->kind = 0;
|
|
$this->kindName = '';
|
|
|
|
$this->password = '';
|
|
$this->mood = '';
|
|
$this->envirBg = '';
|
|
$this->authorBg = '';
|
|
|
|
$this->mapType = '';
|
|
$this->mapStyle = '';
|
|
$this->lightmap = 0;
|
|
$this->titleUid = '';
|
|
|
|
$this->xmlVer = '';
|
|
$this->exeVer = '';
|
|
$this->exeBld = '';
|
|
$this->validated = false;
|
|
$this->songFile = '';
|
|
$this->songUrl = '';
|
|
$this->modName = '';
|
|
$this->modFile = '';
|
|
$this->modUrl = '';
|
|
|
|
$this->thumbLen = 0;
|
|
$this->thumbnail = '';
|
|
$this->comment = '';
|
|
|
|
$this->parseXml = (bool)$parsexml;
|
|
$this->tnImage = (bool)$tnimage;
|
|
if ((bool)$debug)
|
|
$this->enableDebug();
|
|
|
|
$this->setError('GBX map error: ');
|
|
} // __construct
|
|
|
|
/**
|
|
* Process GBX challenge/map file
|
|
*
|
|
* @param String $filename
|
|
* The challenge filename
|
|
*/
|
|
public function processFile($filename)
|
|
{
|
|
$this->loadGBXdata((string)$filename);
|
|
|
|
$this->processGBX();
|
|
} // processFile
|
|
|
|
/**
|
|
* Process GBX challenge/map data
|
|
*
|
|
* @param String $gbxdata
|
|
* The challenge/map data
|
|
*/
|
|
public function processData($gbxdata)
|
|
{
|
|
$this->storeGBXdata((string)$gbxdata);
|
|
|
|
$this->processGBX();
|
|
} // processData
|
|
|
|
// process GBX data
|
|
private function processGBX()
|
|
{
|
|
// supported challenge/map class IDs
|
|
$challclasses = array(
|
|
self::GBX_CHALLENGE_TMF,
|
|
self::GBX_CHALLENGE_TM,
|
|
);
|
|
|
|
$headerSize = $this->checkHeader($challclasses);
|
|
$headerStart = $headerEnd = $this->getGBXptr();
|
|
|
|
// desired challenge/map chunk IDs
|
|
$chunks = array(
|
|
0x002 => 'Info', // TM, MP
|
|
0x003 => 'String', // TM, MP
|
|
0x004 => 'Version', // TM, MP
|
|
0x005 => 'XML', // TM, MP
|
|
0x007 => 'Thumbnl', // TM, MP
|
|
0x008 => 'Author', // MP
|
|
);
|
|
|
|
$chunksList = $this->getChunksList($headerSize, $chunks);
|
|
|
|
$this->getInfoChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
$this->getStringChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
$this->getVersionChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
$this->getXMLChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
$this->getThumbnlChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
$this->getAuthorChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
if ($headerSize != $headerEnd - $headerStart)
|
|
$this->errorOut(sprintf('Header size mismatch: %d <> %d',
|
|
$headerSize, $headerEnd - $headerStart), 16);
|
|
|
|
if ($this->parseXml) {
|
|
if (isset($this->xmlParsed['HEADER']['VERSION']))
|
|
$this->xmlVer = $this->xmlParsed['HEADER']['VERSION'];
|
|
if (isset($this->xmlParsed['HEADER']['EXEVER']))
|
|
$this->exeVer = $this->xmlParsed['HEADER']['EXEVER'];
|
|
if (isset($this->xmlParsed['HEADER']['EXEBUILD']))
|
|
$this->exeBld = $this->xmlParsed['HEADER']['EXEBUILD'];
|
|
if ($this->lightmap == 0 && isset($this->xmlParsed['HEADER']['LIGHTMAP']))
|
|
$this->lightmap = (int)$this->xmlParsed['HEADER']['LIGHTMAP'];
|
|
if ($this->authorZone == '' && isset($this->xmlParsed['IDENT']['AUTHORZONE']))
|
|
$this->authorZone = $this->xmlParsed['IDENT']['AUTHORZONE'];
|
|
if ($this->envir == 'UNKNOWN' && isset($this->xmlParsed['DESC']['ENVIR']))
|
|
$this->envir = $this->xmlParsed['DESC']['ENVIR'];
|
|
if ($this->nbLaps == 0 && isset($this->xmlParsed['DESC']['NBLAPS']))
|
|
$this->nbLaps = (int)$this->xmlParsed['DESC']['NBLAPS'];
|
|
if (isset($this->xmlParsed['DESC']['VALIDATED']))
|
|
$this->validated = (bool)$this->xmlParsed['DESC']['VALIDATED'];
|
|
if (isset($this->xmlParsed['DESC']['MOD']))
|
|
$this->modName = $this->xmlParsed['DESC']['MOD'];
|
|
|
|
// extract optional song & mod filenames
|
|
if (!empty($this->xmlParsed['DEPS'])) {
|
|
for ($i = 0; $i < count($this->xmlParsed['DEPS']); $i++) {
|
|
if (preg_match('/ChallengeMusics\\\\(.+)/', $this->xmlParsed['DEPS'][$i]['FILE'], $path)) {
|
|
$this->songFile = $path[1];
|
|
if (isset($this->xmlParsed['DEPS'][$i]['URL']))
|
|
$this->songUrl = $this->xmlParsed['DEPS'][$i]['URL'];
|
|
} elseif (preg_match('/.+\\\\Mod\\\\.+/', $this->xmlParsed['DEPS'][$i]['FILE'], $path)) {
|
|
$this->modFile = $path[0];
|
|
if (isset($this->xmlParsed['DEPS'][$i]['URL']))
|
|
$this->modUrl = $this->xmlParsed['DEPS'][$i]['URL'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->clearGBXdata();
|
|
} // processGBX
|
|
|
|
/**
|
|
* Get Info chunk from GBX header block
|
|
* @param array $chunksList
|
|
* List of chunk offsets & sizes
|
|
*/
|
|
protected function getInfoChunk(array $chunksList)
|
|
{
|
|
if (!isset($chunksList['Info'])) return;
|
|
|
|
$this->initChunk($chunksList['Info']['off']);
|
|
$version = $this->readInt8();
|
|
$this->debugLog('GBX Info chunk version: ' . $version);
|
|
|
|
if ($version < 3) {
|
|
$this->uid = $this->readLookbackString();
|
|
|
|
$this->envir = $this->readLookbackString();
|
|
$this->author = $this->readLookbackString();
|
|
|
|
$this->name = $this->stripBOM($this->readString());
|
|
}
|
|
$this->moveGBXptr(4); // skip bool 0
|
|
|
|
if ($version >= 1) {
|
|
$this->bronzeTime = $this->readInt32();
|
|
|
|
$this->silverTime = $this->readInt32();
|
|
|
|
$this->goldTime = $this->readInt32();
|
|
|
|
$this->authorTime = $this->readInt32();
|
|
|
|
if ($version == 2)
|
|
$this->moveGBXptr(1); // skip unknown byte
|
|
|
|
if ($version >= 4) {
|
|
$this->cost = $this->readInt32();
|
|
|
|
if ($version >= 5) {
|
|
$this->multiLap = (bool)$this->readInt32();
|
|
|
|
if ($version == 6)
|
|
$this->moveGBXptr(4); // skip unknown bool
|
|
|
|
if ($version >= 7) {
|
|
$this->type = $this->readInt32();
|
|
switch ($this->type) {
|
|
case 0: $this->typeName = 'Race';
|
|
break;
|
|
case 1: $this->typeName = 'Platform';
|
|
break;
|
|
case 2: $this->typeName = 'Puzzle';
|
|
break;
|
|
case 3: $this->typeName = 'Crazy';
|
|
break;
|
|
case 4: $this->typeName = 'Shortcut';
|
|
break;
|
|
case 5: $this->typeName = 'Stunts';
|
|
break;
|
|
case 6: $this->typeName = 'Script';
|
|
break;
|
|
default: $this->typeName = 'UNKNOWN';
|
|
}
|
|
|
|
if ($version >= 9) {
|
|
$this->moveGBXptr(4); // skip int32 0
|
|
|
|
if ($version >= 10) {
|
|
$this->authorScore = $this->readInt32();
|
|
|
|
if ($version >= 11) {
|
|
$this->simpleEdit = (bool)$this->readInt32();
|
|
|
|
if ($version >= 12) {
|
|
$this->moveGBXptr(4); // skip bool 0
|
|
|
|
if ($version >= 13) {
|
|
$this->nbChecks = $this->readInt32();
|
|
|
|
$this->nbLaps = $this->readInt32();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} // getInfoChunk
|
|
|
|
/**
|
|
* Get String chunk from GBX header block
|
|
* @param array $chunksList
|
|
* List of chunk offsets & sizes
|
|
*/
|
|
protected function getStringChunk(array $chunksList)
|
|
{
|
|
if (!isset($chunksList['String'])) return;
|
|
|
|
$this->initChunk($chunksList['String']['off']);
|
|
$version = $this->readInt8();
|
|
$this->debugLog('GBX String chunk version: ' . $version);
|
|
|
|
$this->uid = $this->readLookbackString();
|
|
|
|
$this->envir = $this->readLookbackString();
|
|
$this->author = $this->readLookbackString();
|
|
|
|
$this->name = $this->stripBOM($this->readString());
|
|
|
|
$this->kind = $this->readInt8();
|
|
switch ($this->kind) {
|
|
case 0: $this->kindName = '(internal)EndMarker';
|
|
break;
|
|
case 1: $this->kindName = '(old)Campaign';
|
|
break;
|
|
case 2: $this->kindName = '(old)Puzzle';
|
|
break;
|
|
case 3: $this->kindName = '(old)Retro';
|
|
break;
|
|
case 4: $this->kindName = '(old)TimeAttack';
|
|
break;
|
|
case 5: $this->kindName = '(old)Rounds';
|
|
break;
|
|
case 6: $this->kindName = 'InProgress';
|
|
break;
|
|
case 7: $this->kindName = 'Campaign';
|
|
break;
|
|
case 8: $this->kindName = 'Multi';
|
|
break;
|
|
case 9: $this->kindName = 'Solo';
|
|
break;
|
|
case 10: $this->kindName = 'Site';
|
|
break;
|
|
case 11: $this->kindName = 'SoloNadeo';
|
|
break;
|
|
case 12: $this->kindName = 'MultiNadeo';
|
|
break;
|
|
default: $this->kindName = 'UNKNOWN';
|
|
}
|
|
|
|
if ($version >= 1) {
|
|
$this->moveGBXptr(4); // skip locked
|
|
|
|
$this->password = $this->readString();
|
|
|
|
if ($version >= 2) {
|
|
$this->mood = $this->readLookbackString();
|
|
$this->mood = preg_replace('/([A-Za-z]+)\d*/', '\1', $this->mood);
|
|
|
|
$this->envirBg = $this->readLookbackString();
|
|
$this->authorBg = $this->readLookbackString();
|
|
|
|
if ($version >= 3) {
|
|
$this->moveGBXptr(8); // skip mapOrigin
|
|
|
|
if ($version >= 4) {
|
|
$this->moveGBXptr(8); // skip mapTarget
|
|
|
|
if ($version >= 5) {
|
|
$this->moveGBXptr(16); // skip unknown int128
|
|
|
|
if ($version >= 6) {
|
|
$this->mapType = $this->readString();
|
|
$this->mapStyle = $this->readString();
|
|
|
|
if ($version <= 8)
|
|
$this->moveGBXptr(4); // skip unknown bool
|
|
|
|
if ($version >= 8) {
|
|
$this->moveGBXptr(8); // skip lightmapCacheUID
|
|
|
|
if ($version >= 9) {
|
|
$this->lightmap = $this->readInt8();
|
|
|
|
if ($version >= 11) {
|
|
$this->titleUid = $this->readLookbackString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} // getStringChunk
|
|
|
|
/**
|
|
* Get Version chunk from GBX header block
|
|
* @param array $chunksList
|
|
* List of chunk offsets & sizes
|
|
*/
|
|
protected function getVersionChunk(array $chunksList)
|
|
{
|
|
if (!isset($chunksList['Version'])) return;
|
|
|
|
$this->initChunk($chunksList['Version']['off']);
|
|
$this->headerVersn = $this->readInt32();
|
|
} // getVersionChunk
|
|
|
|
/**
|
|
* Get Thumbnail/Comments chunk from GBX header block
|
|
* @param array $chunksList
|
|
* List of chunk offsets & sizes
|
|
*/
|
|
protected function getThumbnlChunk(array $chunksList)
|
|
{
|
|
if (!isset($chunksList['Thumbnl'])) return;
|
|
|
|
$this->initChunk($chunksList['Thumbnl']['off']);
|
|
$version = $this->readInt32();
|
|
$this->debugLog('GBX Thumbnail chunk version: ' . $version);
|
|
|
|
if ($version == 1) {
|
|
$thumbSize = $this->readInt32();
|
|
$this->debugLog(sprintf('GBX Thumbnail size: %d (%.1f KB)',
|
|
$thumbSize, $thumbSize / 1024));
|
|
|
|
$this->moveGBXptr(strlen('<Thumbnail.jpg>'));
|
|
$this->thumbnail = $this->readData($thumbSize);
|
|
$this->thumbLen = strlen($this->thumbnail);
|
|
$this->moveGBXptr(strlen('</Thumbnail.jpg>'));
|
|
|
|
$this->moveGBXptr(strlen('<Comments>'));
|
|
$this->comment = $this->stripBOM($this->readString());
|
|
$this->moveGBXptr(strlen('</Comments>'));
|
|
|
|
// return extracted thumbnail image?
|
|
if ($this->tnImage && $this->thumbLen > 0) {
|
|
// check for GD/JPEG libraries
|
|
if (function_exists('imagecreatefromjpeg') &&
|
|
function_exists('imagecopyresampled')) {
|
|
// flip thumbnail via temporary file
|
|
$tmp = tempnam(sys_get_temp_dir(), 'gbxflip');
|
|
if (@file_put_contents($tmp, $this->thumbnail) !== false) {
|
|
if ($tn = @imagecreatefromjpeg($tmp)) {
|
|
$tn = $this->imageFlip($tn, self::IMAGE_FLIP_HORIZONTAL);
|
|
if (@imagejpeg($tn, $tmp)) {
|
|
if (($tn = @file_get_contents($tmp)) !== false) {
|
|
$this->thumbnail = $tn;
|
|
}
|
|
}
|
|
}
|
|
unlink($tmp);
|
|
}
|
|
}
|
|
} else {
|
|
$this->thumbnail = '';
|
|
}
|
|
}
|
|
} // getThumbnlChunk
|
|
|
|
} // class GBXChallMapFetcher
|
|
|
|
|
|
/**
|
|
* @class GBXChallengeFetcher
|
|
* @brief Wrapper class for backwards compatibility with the old GBXChallengeFetcher
|
|
* @deprecated Do not use for new development, use GBXChallMapFetcher instead
|
|
*/
|
|
class GBXChallengeFetcher extends GBXChallMapFetcher
|
|
{
|
|
public $authortm, $goldtm, $silvertm, $bronzetm, $ascore, $azone, $multi, $editor,
|
|
$pub, $nblaps, $parsedxml, $xmlver, $exever, $exebld, $songfile, $songurl,
|
|
$modname, $modfile, $modurl;
|
|
|
|
/**
|
|
* Fetches a hell of a lot of data about a GBX challenge
|
|
*
|
|
* @param String $filename
|
|
* The challenge filename (must include full path)
|
|
* @param Boolean $parsexml
|
|
* If true, the script also parses the XML block
|
|
* @param Boolean $tnimage
|
|
* If true, the script also extracts the thumbnail image; if GD/JPEG
|
|
* libraries are present, image will be flipped upright, otherwise
|
|
* it will be in the original upside-down format
|
|
* Warning: this is binary data in JPEG format, 256x256 pixels for
|
|
* TMU/TMF or 512x512 pixels for MP
|
|
* @return GBXChallengeFetcher
|
|
* If $uid is empty, GBX data couldn't be extracted
|
|
*/
|
|
public function __construct($filename, $parsexml = false, $tnimage = false)
|
|
{
|
|
parent::__construct($parsexml, $tnimage, false);
|
|
|
|
try
|
|
{
|
|
$this->processFile($filename);
|
|
|
|
$this->authortm = $this->authorTime;
|
|
$this->goldtm = $this->goldTime;
|
|
$this->silvertm = $this->silverTime;
|
|
$this->bronzetm = $this->bronzeTime;
|
|
$this->ascore = $this->authorScore;
|
|
$this->azone = $this->authorZone;
|
|
$this->multi = $this->multiLap;
|
|
$this->editor = $this->simpleEdit;
|
|
$this->pub = $this->authorBg;
|
|
$this->nblaps = $this->nbLaps;
|
|
$this->parsedxml = $this->xmlParsed;
|
|
$this->xmlver = $this->xmlVer;
|
|
$this->exever = $this->exeVer;
|
|
$this->exebld = $this->exeBld;
|
|
$this->songfile = $this->songFile;
|
|
$this->songurl = $this->songUrl;
|
|
$this->modname = $this->modName;
|
|
$this->modfile = $this->modFile;
|
|
$this->modurl = $this->modUrl;
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
$this->uid = '';
|
|
}
|
|
}
|
|
|
|
} // class GBXChallengeFetcher
|
|
|
|
|
|
/**
|
|
* @class GBXReplayFetcher
|
|
* @brief The class that fetches all GBX replay info
|
|
* @note The interface for GBXReplayFetcher has changed compared to the old class,
|
|
* but there is no wrapper because no third-party XASECO[2] plugins used that
|
|
*/
|
|
class GBXReplayFetcher extends GBXBaseFetcher
|
|
{
|
|
public $uid, $envir, $author, $replay, $nickname, $login, $titleUid;
|
|
public $xmlVer, $exeVer, $exeBld, $respawns, $stuntScore, $validable,
|
|
$cpsCur, $cpsLap;
|
|
|
|
/**
|
|
* Instantiate GBX replay fetcher
|
|
*
|
|
* @param Boolean $parsexml
|
|
* If true, the fetcher also parses the XML block
|
|
* @param Boolean $debug
|
|
* If true, the fetcher prints debug logging to stderr
|
|
* @return GBXReplayFetcher
|
|
* If GBX data couldn't be extracted, an Exception is thrown with
|
|
* the error message & code
|
|
*/
|
|
public function __construct($parsexml = false, $debug = false)
|
|
{
|
|
parent::__construct();
|
|
|
|
$this->uid = '';
|
|
$this->envir = '';
|
|
$this->author = '';
|
|
$this->replay = 0;
|
|
$this->nickname = '';
|
|
$this->login = '';
|
|
$this->titleUid = '';
|
|
|
|
$this->xmlVer = '';
|
|
$this->exeVer = '';
|
|
$this->exeBld = '';
|
|
$this->respawns = 0;
|
|
$this->stuntScore = 0;
|
|
$this->validable = false;
|
|
$this->cpsCur = 0;
|
|
$this->cpsLap = 0;
|
|
|
|
$this->parseXml = (bool)$parsexml;
|
|
if ((bool)$debug)
|
|
$this->enableDebug();
|
|
|
|
$this->setError('GBX replay error: ');
|
|
} // __construct
|
|
|
|
/**
|
|
* Process GBX replay file
|
|
*
|
|
* @param String $filename
|
|
* The replay filename
|
|
*/
|
|
public function processFile($filename)
|
|
{
|
|
$this->loadGBXdata((string)$filename);
|
|
|
|
$this->processGBX();
|
|
} // processFile
|
|
|
|
/**
|
|
* Process GBX replay data
|
|
*
|
|
* @param String $gbxdata
|
|
* The replay data
|
|
*/
|
|
public function processData($gbxdata)
|
|
{
|
|
$this->storeGBXdata((string)$gbxdata);
|
|
|
|
$this->processGBX();
|
|
} // processData
|
|
|
|
// process GBX data
|
|
private function processGBX()
|
|
{
|
|
// supported replay class IDs
|
|
$replayclasses = array(
|
|
self::GBX_AUTOSAVE_TMF,
|
|
self::GBX_AUTOSAVE_TM,
|
|
self::GBX_REPLAY_TM,
|
|
);
|
|
|
|
$headerSize = $this->checkHeader($replayclasses);
|
|
$headerStart = $headerEnd = $this->getGBXptr();
|
|
|
|
// desired replay chunk IDs
|
|
$chunks = array(
|
|
0x000 => 'String', // TM, MP
|
|
0x001 => 'XML', // TM, MP
|
|
0x002 => 'Author', // MP
|
|
);
|
|
|
|
$chunksList = $this->getChunksList($headerSize, $chunks);
|
|
|
|
$this->getStringChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
$this->getXMLChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
$this->getAuthorChunk($chunksList);
|
|
$headerEnd = max($headerEnd, $this->getGBXptr());
|
|
|
|
if ($headerSize != $headerEnd - $headerStart)
|
|
$this->errorOut(sprintf('Header size mismatch: %d <> %d',
|
|
$headerSize, $headerEnd - $headerStart), 20);
|
|
|
|
if ($this->parseXml) {
|
|
if (isset($this->xmlParsed['HEADER']['VERSION']))
|
|
$this->xmlVer = $this->xmlParsed['HEADER']['VERSION'];
|
|
if (isset($this->xmlParsed['HEADER']['EXEVER']))
|
|
$this->exeVer = $this->xmlParsed['HEADER']['EXEVER'];
|
|
if (isset($this->xmlParsed['HEADER']['EXEBUILD']))
|
|
$this->exeBld = $this->xmlParsed['HEADER']['EXEBUILD'];
|
|
if (isset($this->xmlParsed['TIMES']['RESPAWNS']))
|
|
$this->respawns = (int)$this->xmlParsed['TIMES']['RESPAWNS'];
|
|
if (isset($this->xmlParsed['TIMES']['STUNTSCORE']))
|
|
$this->stuntScore = (int)$this->xmlParsed['TIMES']['STUNTSCORE'];
|
|
if (isset($this->xmlParsed['TIMES']['VALIDABLE']))
|
|
$this->validable = (bool)$this->xmlParsed['TIMES']['VALIDABLE'];
|
|
if (isset($this->xmlParsed['CHECKPOINTS']['CUR']))
|
|
$this->cpsCur = (int)$this->xmlParsed['CHECKPOINTS']['CUR'];
|
|
if (isset($this->xmlParsed['CHECKPOINTS']['ONELAP']))
|
|
$this->cpsLap = (int)$this->xmlParsed['CHECKPOINTS']['ONELAP'];
|
|
}
|
|
|
|
$this->clearGBXdata();
|
|
} // processGBX
|
|
|
|
/**
|
|
* Get String chunk from GBX header block
|
|
* @param array $chunksList
|
|
* List of chunk offsets & sizes
|
|
*/
|
|
protected function getStringChunk(array $chunksList)
|
|
{
|
|
if (!isset($chunksList['String'])) return;
|
|
|
|
$this->initChunk($chunksList['String']['off']);
|
|
$version = $this->readInt32();
|
|
$this->debugLog('GBX String chunk version: ' . $version);
|
|
|
|
if ($version >= 2) {
|
|
$this->uid = $this->readLookbackString();
|
|
|
|
$this->envir = $this->readLookbackString();
|
|
$this->author = $this->readLookbackString();
|
|
|
|
$this->replay = $this->readInt32();
|
|
|
|
$this->nickname = $this->stripBOM($this->readString());
|
|
|
|
if ($version >= 6) {
|
|
$this->login = $this->readString();
|
|
|
|
if ($version >= 8) {
|
|
$this->moveGBXptr(1); // skip unknown byte
|
|
|
|
$this->titleUid = $this->readLookbackString();
|
|
}
|
|
}
|
|
}
|
|
} // getStringChunk
|
|
|
|
} // class GBXReplayFetcher
|
|
|
|
|
|
/**
|
|
* @class GBXPackFetcher
|
|
* @brief The class that fetches all GBX pack info
|
|
*/
|
|
class GBXPackFetcher extends GBXBaseFetcher
|
|
{
|
|
public $headerVersn, $flags, $infoMlUrl, $creatDate, $comment, $titleId,
|
|
$usageSubdir, $buildInfo, $authorUrl, $exeVer, $exeBld, $xmlDate;
|
|
|
|
/**
|
|
* Read Windows FileTime and convert to Unix timestamp
|
|
* Filetime = 64-bit value with the number of 100-nsec intervals since Jan 1, 1601 (UTC)
|
|
* Based on http://www.mysqlperformanceblog.com/2007/03/27/integers-in-php-running-with-scissors-and-portability/
|
|
* @return Unix timestamp, or -1 on error
|
|
*/
|
|
private function readFiletime()
|
|
{
|
|
// Unix epoch (1970-01-01) - Windows epoch (1601-01-01) in 100ns units
|
|
$EPOCHDIFF = '116444735995904000';
|
|
$UINT32MAX = '4294967296';
|
|
$USEC2SEC = 1000000;
|
|
|
|
$lo = $this->readInt32();
|
|
$hi = $this->readInt32();
|
|
|
|
// check for 64-bit platform
|
|
if (PHP_INT_SIZE >= 8) {
|
|
// use native math
|
|
if ($lo < 0) $lo += (1 << 32);
|
|
$date = ($hi << 32) + $lo;
|
|
$this->debugLog(sprintf('PAK CreationDate source: %016x', $date));
|
|
if ($date == 0) return -1;
|
|
|
|
// convert to Unix timestamp in usec
|
|
$stamp = ($date - (int)$EPOCHDIFF) / 10;
|
|
$this->debugLog(sprintf('PAK CreationDate 64-bit: %u.%06u',
|
|
$stamp / $USEC2SEC, $stamp % $USEC2SEC));
|
|
return (int)($stamp / $USEC2SEC);
|
|
|
|
// check for 32-bit platform
|
|
} elseif (PHP_INT_SIZE >= 4) {
|
|
$this->debugLog(sprintf('PAK CreationDate source: %08x%08x', $hi, $lo));
|
|
if ($lo == 0 && $hi == 0) return -1;
|
|
|
|
// workaround signed/unsigned braindamage on x32
|
|
$lo = sprintf('%u', $lo);
|
|
$hi = sprintf('%u', $hi);
|
|
|
|
// try and use GMP
|
|
if (function_exists('gmp_mul')) {
|
|
$date = gmp_add(gmp_mul($hi, $UINT32MAX), $lo);
|
|
// convert to Unix timestamp in usec
|
|
$stamp = gmp_div(gmp_sub($date, $EPOCHDIFF), 10);
|
|
$stamp = gmp_div_qr($stamp, $USEC2SEC);
|
|
$this->debugLog(sprintf('PAK CreationDate GNU MP: %u.%06u',
|
|
gmp_strval($stamp[0]), gmp_strval($stamp[1])));
|
|
return (int)gmp_strval($stamp[0]);
|
|
}
|
|
|
|
// try and use BC Math
|
|
if (function_exists('bcmul')) {
|
|
$date = bcadd(bcmul($hi, $UINT32MAX), $lo);
|
|
// convert to Unix timestamp in usec
|
|
$stamp = bcdiv(bcsub($date, $EPOCHDIFF), 10, 0);
|
|
$this->debugLog(sprintf('PAK CreationDate BCMath: %u.%06u',
|
|
bcdiv($stamp, $USEC2SEC), bcmod($stamp, $USEC2SEC)));
|
|
return (int)bcdiv($stamp, $USEC2SEC);
|
|
}
|
|
|
|
// compute everything manually
|
|
$a = substr($hi, 0, -5);
|
|
$b = substr($hi, -5);
|
|
// hope that float precision is enough
|
|
$ac = $a * 42949;
|
|
$bd = $b * 67296;
|
|
$adbc = $a * 67296 + $b * 42949;
|
|
$r4 = substr($bd, -5) + substr($lo, -5);
|
|
$r3 = substr($bd, 0, -5) + substr($adbc, -5) + substr($lo, 0, -5);
|
|
$r2 = substr($adbc, 0, -5) + substr($ac, -5);
|
|
$r1 = substr($ac, 0, -5);
|
|
while ($r4 >= 100000) { $r4 -= 100000; $r3++; }
|
|
while ($r3 >= 100000) { $r3 -= 100000; $r2++; }
|
|
while ($r2 >= 100000) { $r2 -= 100000; $r1++; }
|
|
$date = ltrim(sprintf('%d%05d%05d%05d', $r1, $r2, $r3, $r4), '0');
|
|
|
|
// convert to Unix timestamp in usec
|
|
$r3 = substr($date, -6) - substr($EPOCHDIFF, -6);
|
|
$r2 = substr($date, -12, 6) - substr($EPOCHDIFF, -12, 6);
|
|
$r1 = substr($date, -18, 6) - substr($EPOCHDIFF, -18, 6);
|
|
if ($r3 < 0) { $r3 += 1000000; $r2--; }
|
|
if ($r2 < 0) { $r2 += 1000000; $r1--; }
|
|
$stamp = substr(sprintf('%d%06d%06d', $r1, $r2, $r3), 0, -1);
|
|
$this->debugLog(sprintf('PAK CreationDate manual: %s.%s',
|
|
substr($stamp, 0, -6), substr($stamp, -6)));
|
|
return (int)substr($stamp, 0, -6);
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instantiate GBX pack fetcher
|
|
*
|
|
* @param Boolean $parsexml
|
|
* If true, the fetcher also parses the XML block
|
|
* @param Boolean $debug
|
|
* If true, the fetcher prints debug logging to stderr
|
|
* @return GBXPackFetcher
|
|
* If GBX data couldn't be extracted, an Exception is thrown with
|
|
* the error message & code
|
|
*/
|
|
public function __construct($parsexml = false, $debug = false)
|
|
{
|
|
parent::__construct();
|
|
|
|
$this->headerVersn = 0;
|
|
$this->flags = 0;
|
|
$this->infoMlUrl = '';
|
|
$this->creatDate = -1;
|
|
$this->comment = '';
|
|
$this->titleId = '';
|
|
$this->usageSubdir = '';
|
|
$this->buildInfo = '';
|
|
$this->authorUrl = '';
|
|
$this->exeVer = '';
|
|
$this->exeBld = '';
|
|
$this->xmlDate = '';
|
|
|
|
$this->parseXml = (bool)$parsexml;
|
|
if ((bool)$debug)
|
|
$this->enableDebug();
|
|
|
|
$this->setError('GBX pack error: ');
|
|
} // __construct
|
|
|
|
/**
|
|
* Process GBX pack file
|
|
*
|
|
* @param String $filename
|
|
* The pack filename
|
|
*/
|
|
public function processFile($filename)
|
|
{
|
|
$this->loadGBXdata((string)$filename);
|
|
|
|
$this->processGBX();
|
|
} // processFile
|
|
|
|
/**
|
|
* Process GBX pack data
|
|
*
|
|
* @param String $gbxdata
|
|
* The pack data
|
|
*/
|
|
public function processData($gbxdata)
|
|
{
|
|
$this->storeGBXdata((string)$gbxdata);
|
|
|
|
$this->processGBX();
|
|
} // processData
|
|
|
|
// process GBX data
|
|
private function processGBX()
|
|
{
|
|
// check magic header
|
|
$data = $this->readData(8);
|
|
if ($data != 'NadeoPak')
|
|
$this->errorOut('No magic NadeoPak header', 5);
|
|
|
|
$this->headerVersn = $this->readInt32();
|
|
if ($this->headerVersn < 6)
|
|
$this->errorOut(sprintf('Pack version %d not supported', $this->headerVersn), 24);
|
|
|
|
$this->moveGBXptr(32); // skip ContentsChecksum
|
|
|
|
$this->flags = $this->readInt32();
|
|
|
|
if ($this->headerVersn >= 7) {
|
|
$this->getAuthorFields();
|
|
|
|
if ($this->headerVersn < 9) {
|
|
$this->comment = $this->stripBOM($this->readString());
|
|
|
|
$this->moveGBXptr(16); // skip unused uint128
|
|
|
|
if ($this->headerVersn >= 8) {
|
|
$this->buildInfo = $this->readString();
|
|
|
|
$this->authorUrl = $this->readString();
|
|
}
|
|
|
|
} else { // >= 9
|
|
$this->infoMlUrl = $this->readString();
|
|
|
|
$this->creatDate = $this->readFiletime();
|
|
|
|
$this->comment = $this->stripBOM($this->readString());
|
|
|
|
if ($this->headerVersn >= 12) {
|
|
$this->xml = $this->readString();
|
|
$this->titleId = $this->readString();
|
|
|
|
if ($this->parseXml && $this->xml != '') {
|
|
$this->parseXMLstring();
|
|
|
|
if (isset($this->xmlParsed['HEADER']['EXEVER']))
|
|
$this->exeVer = $this->xmlParsed['HEADER']['EXEVER'];
|
|
if (isset($this->xmlParsed['HEADER']['EXEBUILD']))
|
|
$this->exeBld = $this->xmlParsed['HEADER']['EXEBUILD'];
|
|
if (isset($this->xmlParsed['IDENT']['CREATIONDATE']))
|
|
$this->xmlDate = $this->xmlParsed['IDENT']['CREATIONDATE'];
|
|
}
|
|
}
|
|
|
|
$this->usageSubdir = $this->readString();
|
|
|
|
$this->buildInfo = $this->readString();
|
|
|
|
$this->moveGBXptr(16); // skip unused uint128
|
|
// if ($this->headerVersn >= 10) // skip encrypted IncludedPacks
|
|
}
|
|
}
|
|
|
|
$this->clearGBXdata();
|
|
} // processGBX
|
|
|
|
} // class GBXPackFetcher
|
|
|