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 &$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
 | |
| 
 |