* @version 0.0.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2012 Hub Developer Team * @license GNU GPL 3.0 or any newer version * @link http://www.shipsimu.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ class PackageFragmenter extends BaseHubSystem implements Fragmentable, Registerable { /** * Cached chunk size in bits */ private $chunkSize = 0; /** * Array for chunks */ private $chunks = array(); /** * Array for chunk hashes */ private $chunkHashes = array(); /** * Array for chunk pointers */ private $chunkPointers = array(); /** * Array for processed packages */ private $processedPackages = array(); /** * Serial numbers (array key is final hash) */ private $serialNumber = array(); /** * Maximum possible serial number, "cache" for speeding up things */ private $maxSerialNumber = 0; /** * Length of largest possible serial number */ const MAX_SERIAL_LENGTH = 8; /** * Separator between chunk data, serial number and chunk hash */ const CHUNK_DATA_HASH_SEPARATOR = '@'; /** * SEPARATOR for all chunk hashes */ const CHUNK_HASH_SEPARATOR = ';'; /** * SEPARATOR between two chunks */ const CHUNK_SEPARATOR = '|'; /** * Identifier for hash chunk */ const HASH_CHUNK_IDENTIFIER = 'HASH-CHUNK:'; /** * Identifier for end-of-package marker */ const END_OF_PACKAGE_IDENTIFIER = 'EOP:'; /** * Protected constructor * * @return void */ protected function __construct () { // Call parent constructor parent::__construct(__CLASS__); // Init this fragmenter $this->initFragmenter(); } /** * Creates an instance of this class * * @return $fragmenterInstance An instance of a Fragmentable class */ public static final function createPackageFragmenter () { // Get new instance $fragmenterInstance = new PackageFragmenter(); // Get a crypto instance and set it here $cryptoInstance = ObjectFactory::createObjectByConfiguredName('crypto_class'); $fragmenterInstance->setCryptoInstance($cryptoInstance); // Return the prepared instance return $fragmenterInstance; } /** * Initializes this fragmenter * * @return void */ private function initFragmenter () { // Load some configuration entries and "cache" them: // - Chunk size in bits $this->chunkSize = $this->getConfigInstance()->getConfigEntry('package_chunk_size'); // - Maximum serial number $this->maxSerialNumber = $this->hex2dec(str_repeat('f', self::MAX_SERIAL_LENGTH)); } /** * Initializes the pointer for given final hash * * @param $finalHash Final hash to initialize pointer for * @return void */ private function initPointer ($finalHash) { $this->chunkPointers[$finalHash] = 0; } /** * "Getter" for processedPackages array index * * @param $packageData Raw package data array * @return $index Array index for processedPackages */ private function getProcessedPackagesIndex (array $packageData) { return ( $packageData[NetworkPackage::PACKAGE_DATA_SENDER] . NetworkPackage::PACKAGE_DATA_SEPARATOR . $packageData[NetworkPackage::PACKAGE_DATA_RECIPIENT] . NetworkPackage::PACKAGE_DATA_SEPARATOR . $packageData[NetworkPackage::PACKAGE_DATA_CONTENT] . NetworkPackage::PACKAGE_DATA_SEPARATOR ); } /** * Checks whether the given package data is already processed by this fragmenter * * @param $packageData Raw package data array * @return $isProcessed Whether the package has been fragmented */ private function isPackageProcessed (array $packageData) { // Get array index $index = $this->getProcessedPackagesIndex($packageData); // Is the array index there? $isProcessed = ( (isset($this->processedPackages[$index])) && ($this->processedPackages[$index] === TRUE) ); // Return it return $isProcessed; } /** * Marks the given package data as processed by this fragmenter * * @param $packageData Raw package data array * @return void */ private function markPackageDataProcessed (array $packageData) { // Remember it (until we may remove it) $this->processedPackages[$this->getProcessedPackagesIndex($packageData)] = TRUE; } /** * Getter for final hash from given package data * * @param $packageData Raw package data array * @return $finalHash Final hash for package data */ private function getFinalHashFromPackageData (array $packageData) { // Make sure it is there assert(isset($this->processedPackages[$this->getProcessedPackagesIndex($packageData)])); // Return it return $this->processedPackages[$this->getProcessedPackagesIndex($packageData)]; } /** * Get next chunk pointer for given final hash * * @param $finalHash Final hash to get current pointer for */ private function getCurrentChunkPointer ($finalHash) { // Is the final hash valid? assert(strlen($finalHash) > 0); // Is the pointer already initialized? //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: finalHash[' . gettype($finalHash) . ']=' . $finalHash); assert(isset($this->chunkPointers[$finalHash])); // Return it return $this->chunkPointers[$finalHash]; } /** * Advance the chunk pointer for given final hash * * @param $finalHash Final hash to advance the pointer for */ private function nextChunkPointer ($finalHash) { // Is the pointer already initialized? assert(isset($this->chunkPointers[$finalHash])); // Count one up //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: finalHash[' . gettype($finalHash) . ']=' . $finalHash); $this->chunkPointers[$finalHash]++; } /** * "Getter" for data chunk size of given hash. * * @param $hash Hash to substract it's length * @return $dataChunkSize The chunk size */ private function getDataChunkSizeFromHash ($hash) { // Calculate real (data) chunk size $dataChunkSize = ( // Real chunk size ($this->chunkSize / 8) - // Hash size strlen($hash) - // Length of sperators (strlen(self::CHUNK_DATA_HASH_SEPARATOR) * 2) - // Length of max serial number self::MAX_SERIAL_LENGTH ); // This should be larger than zero bytes assert($dataChunkSize > 0); // Return it return $dataChunkSize; } /** * Generates a hash from raw data * * @param $rawData Raw data bytes to hash * @return $hash Hash from the raw data * @todo Implement a way to send non-announcement packages with extra-salt */ private function generateHashFromRawData ($rawData) { /* * Get the crypto instance and hash the data with no extra salt because * the other peer doesn't have *this* peer's salt. */ $hash = $this->getCryptoInstance()->hashString($rawData, '', FALSE); // Return it return $hash; } /** * Appends an end-of-package chunk to the chunk list for given chunk and * final hash. As of 23-March-2012 the format of this chunk will be as any * regular one to keep things easy (KISS) in ChunkHandler class. * * @param $lastChunk Last chunk raw data * @param $finalHash Final hash for raw (unencoded) data * @return void */ private function appendEndOfPackageChunk ($lastChunk, $finalHash) { // Generate end-of-package marker $chunkData = self::END_OF_PACKAGE_IDENTIFIER . $finalHash . self::CHUNK_HASH_SEPARATOR . $this->generateHashFromRawData($lastChunk); // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: Adding EOP chunk with size of ' . strlen($chunkData) . ',finalHash=' . $finalHash . ' ...'); // Add it as regular chunk $this->addChunkData($finalHash, $chunkData); } /** * Splits the given encoded data into smaller chunks, the size of the final * and the SEPARATOR is being subtracted from chunk size to fit it into a * TCP package (512 bytes). * * @param $rawData Raw data string * @param $finalHash Final hash from the raw data * @return void */ private function splitEncodedDataIntoChunks ($rawData, $finalHash) { // Make sure final hashes with at least 32 bytes can pass assert(strlen($finalHash) >= 32); // Calculate real (data) chunk size $dataChunkSize = $this->getDataChunkSizeFromHash($finalHash); //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: dataChunkSize=' . $dataChunkSize); // Init variables $chunkHash = ''; $chunkData = ''; // Now split it up for ($idx = 0; $idx < strlen($rawData); $idx += $dataChunkSize) { // Get the next chunk $chunkData = substr($rawData, $idx, $dataChunkSize); // Add the chunk to the propper array and do all the stuff there $this->addChunkData($finalHash, $chunkData); } // END - for // Debug output //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: Raw data of ' . strlen($rawData) . ' bytes has been fragmented into ' . count($this->chunks[$finalHash]) . ' chunk(s).'); // Add end-of-package chunk $this->appendEndOfPackageChunk($chunkData, $finalHash); } /** * Adds the given chunk (raw data) to the proper array and hashes it for * later verfication. * * @param $finalHash Final hash for faster processing * @param $chunkData Raw chunk data * @param $prepend Whether append (default) or prepend the chunk * @return void */ private function addChunkData ($finalHash, $chunkData, $prepend = FALSE) { // Hash it $rawDataHash = $this->getCryptoInstance()->hashString($chunkData, '', FALSE); // Prepend the hash to the chunk $rawData = ( $rawDataHash . self::CHUNK_DATA_HASH_SEPARATOR . $this->getNextHexSerialNumber($finalHash) . self::CHUNK_DATA_HASH_SEPARATOR . $chunkData . self::CHUNK_SEPARATOR ); // Make sure the chunk is not larger than a TCP package can hold //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: assert: ' . strlen($rawData) . '/' . NetworkPackage::TCP_PACKAGE_SIZE . ' ...'); // @TODO This assert broke packages where the hash chunk was very large: assert(strlen($rawData) <= NetworkPackage::TCP_PACKAGE_SIZE); // Add it to the array if ($prepend === TRUE) { // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: Prepending ' . strlen($rawData) . ' bytes of a chunk, finalHash=' . $finalHash . ' ...'); array_unshift($this->chunkHashes[$finalHash], $rawDataHash); array_unshift($this->chunks[$finalHash] , $rawData); } else { // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: Appending ' . strlen($rawData) . ' bytes of a chunk, finalHash=' . $finalHash . ' ...'); // Is the array there? if (!isset($this->chunks[$finalHash])) { // Then initialize it $this->chunks[$finalHash] = array(); $this->chunkHashes[$finalHash] = array(); } // END - if // Add both array_push($this->chunks[$finalHash] , $rawData); array_push($this->chunkHashes[$finalHash], $rawDataHash); } } /** * Prepends a chunk (or more) with all hashes from all chunks + final chunk. * * @param $finalHash Final hash from the raw data * @return void */ private function prependHashChunk ($finalHash) { // "Implode" the whole array of hashes into one string $rawData = self::HASH_CHUNK_IDENTIFIER . implode(self::CHUNK_HASH_SEPARATOR, $this->chunkHashes[$finalHash]); // Prepend chunk $this->addChunkData($finalHash, $rawData, TRUE); } /** * "Getter" for the next hexadecimal-encoded serial number * * @param $finalHash Final hash * @return $encodedSerialNumber The next hexadecimal-encoded serial number */ public function getNextHexSerialNumber ($finalHash) { // Assert on maximum serial number length assert($this->serialNumber[$finalHash] <= $this->maxSerialNumber); // Encode the current serial number /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: serialNumber[' . $finalHash . ']=' . $this->serialNumber[$finalHash]); $encodedSerialNumber = $this->dec2Hex($this->serialNumber[$finalHash], self::MAX_SERIAL_LENGTH); // Count one up $this->serialNumber[$finalHash]++; // Return the encoded serial number /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: encodedSerialNumber=' . $encodedSerialNumber); return $encodedSerialNumber; } /** * This method does "implode" the given package data array into one long * string, splits it into small chunks, adds a serial number and checksum * to all chunks and prepends a chunk with all hashes only in it. It will * return the final hash for faster processing of packages. * * @param $packageData Raw package data array * @param $helperInstance An instance of a ConnectionHelper class * @return $finalHash Final hash for faster processing * @todo $helperInstance is unused */ public function fragmentPackageArray (array $packageData, ConnectionHelper $helperInstance) { // Is this package already fragmented? if (!$this->isPackageProcessed($packageData)) { // Remove package status and protocol, the recipient doesn't need this unset($packageData[NetworkPackage::PACKAGE_DATA_STATUS]); unset($packageData[NetworkPackage::PACKAGE_DATA_PROTOCOL]); // First we need to "implode" the array $rawData = implode(NetworkPackage::PACKAGE_DATA_SEPARATOR, $packageData); // Generate the final hash from the raw data (not encoded!) $finalHash = $this->generateHashFromRawData($rawData); // Remember it $this->processedPackages[$this->getProcessedPackagesIndex($packageData)] = $finalHash; // Init pointer and reset serial number $this->initPointer($finalHash); $this->resetSerialNumber($finalHash); // Split the encoded data into smaller chunks $this->splitEncodedDataIntoChunks($rawData, $finalHash); // Prepend a chunk with all hashes together $this->prependHashChunk($finalHash); // Mark the package as fragmented $this->markPackageDataProcessed($packageData); } else { // Get the final hash from the package data $finalHash = $this->getFinalHashFromPackageData($packageData); } // Return final hash //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: finalHash[' . gettype($finalHash) . ']=' . $finalHash); return $finalHash; } /** * This method gets the next chunk from the internal FIFO which should be * sent to the given recipient. It will return an associative array where * the key is the chunk hash and value the raw chunk data. * * @param $finalHash Final hash for faster lookup * @return $rawDataChunk Raw package data chunk * @throws AssertionException If $finalHash was not 'TRUE' */ public function getNextRawDataChunk ($finalHash) { // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: finalHash[' . gettype($finalHash) . ']=' . $finalHash); try { // Get current chunk index $current = $this->getCurrentChunkPointer($finalHash); } catch (AssertionException $e) { // This may happen when the final hash is TRUE if ($finalHash === TRUE) { // Set current to null $current = NULL; } else { // Throw the exception throw $e; } } // If there is no entry left, return an empty array if ((!isset($this->chunkHashes[$finalHash][$current])) || (!isset($this->chunks[$finalHash][$current]))) { // No more entries found //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput(__METHOD__. ': finalHash=' . $finalHash . ',current=' . $current . ' - No more entries found!'); return array(); } // END - if // Debug message //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput(__METHOD__. ': finalHash=' . $finalHash . ',current=' . $current . ',chunkHashes()=' . count($this->chunkHashes[$finalHash]) .' - Entry choosen ...'); // Generate the array $rawDataChunk = array( $this->chunkHashes[$finalHash][$current] => $this->chunks[$finalHash][$current] ); // Count one index up $this->nextChunkPointer($finalHash); // Return the chunk array return $rawDataChunk; } /** * Resets the serial number to zero for given final hash * * @param $finalHash Final hash to reset counter for * @return void */ public function resetSerialNumber ($finalHash) { // Final hash must be set assert((is_string($finalHash)) && (!empty($finalHash))); // Reset serial number $this->serialNumber[$finalHash] = 0; } } // [EOF] ?>