--- /dev/null
+<?php
+/**
+ * A PackageFragmenter class to fragment package data into smaller chunks for
+ * delivery. This class calculates a final hash on the raw input data and
+ * fragments the data into smaller chunks after it has been encoded by a
+ * "outgoing encoding stream".
+ *
+ * All chunks are extended with a hash and a serial number to make it later
+ * easier to verify them and put them back in the right order and to, if
+ * required, request a re-delivery of an invalid chunk (e.g. hash didn't match).
+ * Also an "end-of-package" marker is being added as the last chunk to mark the
+ * end of of the whole package submission.
+ *
+ * @author Roland Haeder <webmaster@shipsimu.org>
+ * @version 0.0.0
+ * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2015 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 <http://www.gnu.org/licenses/>.
+ */
+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(isset($this->serialNumber[$finalHash]));
+ 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)) {
+ // 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/set serial number
+ $this->serialNumber[$finalHash] = 0;
+ }
+}
+
+// [EOF]
+?>