3 * A PackageFragmenter class to fragment package data into smaller chunks for
4 * delivery. This class calculates a final hash on the raw input data and
5 * fragments the data into smaller chunks after it has been encoded by a
6 * "outgoing encoding stream".
8 * All chunks are extended with a hash and a serial number to make it later
9 * easier to verify them and put them back in the right order and to, if
10 * required, request a re-delivery of an invalid chunk (e.g. hash didn't match).
11 * Also an "end-of-package" marker is being added as the last chunk to mark the
12 * end of of the whole package submission.
14 * @author Roland Haeder <webmaster@ship-simu.org>
16 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2011 Hub Developer Team
17 * @license GNU GPL 3.0 or any newer version
18 * @link http://www.ship-simu.org
20 * This program is free software: you can redistribute it and/or modify
21 * it under the terms of the GNU General Public License as published by
22 * the Free Software Foundation, either version 3 of the License, or
23 * (at your option) any later version.
25 * This program is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 * GNU General Public License for more details.
30 * You should have received a copy of the GNU General Public License
31 * along with this program. If not, see <http://www.gnu.org/licenses/>.
33 class PackageFragmenter extends BaseFrameworkSystem implements Fragmentable, Registerable {
35 * Cached chunk size in bits
37 private $chunkSize = 0;
42 private $chunks = array();
45 * Array for chunk hashes
47 private $chunkHashes = array();
50 * Array for chunk pointers
52 private $chunkPointers = array();
55 * Array for processed packages
57 private $processedPackages = array();
62 private $serialNumber = 0x00000000;
65 * Length of largest possible serial number
67 private $maxSerialLength = 8;
70 * Maximum possible serial number
72 private $maxSerialNumber = 0;
75 * Seperator between chunk data, serial number and chunk hash
77 const CHUNK_DATA_HASH_SEPERATOR = '@';
80 * Seperator for all chunk hashes
82 const CHUNK_HASH_SEPERATOR = ';';
85 * Seperator between two chunks
87 const CHUNK_SEPERATOR = '|';
90 * Identifier for hash chunk
92 const HASH_CHUNK_IDENTIFIER = 'HASH-CHUNK:';
95 * Identifier for end-of-package marker
97 const END_OF_PACKAGE_IDENTIFIER = 'EOP:';
100 * Protected constructor
104 protected function __construct () {
105 // Call parent constructor
106 parent::__construct(__CLASS__);
108 // Init this fragmenter
109 $this->initFragmenter();
113 * Creates an instance of this class
115 * @return $fragmenterInstance An instance of a Fragmentable class
117 public static final function createPackageFragmenter () {
119 $fragmenterInstance = new PackageFragmenter();
121 // And also a crypto instance (for our encrypted messages)
122 $cryptoInstance = ObjectFactory::createObjectByConfiguredName('crypto_class');
123 $fragmenterInstance->setCryptoInstance($cryptoInstance);
125 // Return the prepared instance
126 return $fragmenterInstance;
130 * Initializes this fragmenter
134 private function initFragmenter () {
135 // Load some configuration entries and "cache" them:
136 // - Chunk size in bits
137 $this->chunkSize = $this->getConfigInstance()->getConfigEntry('package_chunk_size');
139 // - Maximum serial number
140 $this->maxSerialNumber = $this->hex2dec(str_repeat('f', $this->maxSerialLength));
144 * Initializes the pointer for given final hash
146 * @param $finalHash Final hash to initialize pointer for
149 private function initPointer ($finalHash) {
150 $this->chunkPointers[$finalHash] = 0;
154 * "Getter" for processedPackages array index
156 * @param $packageData Raw package data array
157 * @return $index Array index for processedPackages
159 private function getProcessedPackagesIndex (array $packageData) {
161 $packageData[NetworkPackage::PACKAGE_DATA_SENDER] . NetworkPackage::PACKAGE_DATA_SEPERATOR .
162 $packageData[NetworkPackage::PACKAGE_DATA_RECIPIENT] . NetworkPackage::PACKAGE_DATA_SEPERATOR .
163 $packageData[NetworkPackage::PACKAGE_DATA_CONTENT] . NetworkPackage::PACKAGE_DATA_SEPERATOR
168 * Checks wether the given package data is already processed by this fragmenter
170 * @param $packageData Raw package data array
171 * @return $isProcessed Wether the package has been fragmented
173 private function isPackageProcessed (array $packageData) {
175 $index = $this->getProcessedPackagesIndex($packageData);
177 // Is the array index there?
179 (isset($this->processedPackages[$index])) &&
180 ($this->processedPackages[$index] === true)
188 * Marks the given package data as processed by this fragmenter
190 * @param $packageData Raw package data array
193 private function markPackageDataProcessed (array $packageData) {
194 // Remember it (until we may remove it)
195 $this->processedPackages[$this->getProcessedPackagesIndex($packageData)] = true;
199 * Getter for final hash from given package data
201 * @param $packageData Raw package data array
202 * @return $finalHash Final hash for package data
204 private function getFinalHashFromPackageData (array $packageData) {
205 // Make sure it is there
206 assert(isset($this->processedPackages[$this->getProcessedPackagesIndex($packageData)]));
209 return $this->processedPackages[$this->getProcessedPackagesIndex($packageData)];
213 * Get next chunk pointer for given final hash
215 * @param $finalHash Final hash to get current pointer for
217 private function getCurrentChunkPointer ($finalHash) {
218 // Is the final hash valid?
219 assert(strlen($finalHash) > 0);
221 // Is the pointer already initialized?
222 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: finalHash=' . $finalHash);
223 assert(isset($this->chunkPointers[$finalHash]));
226 return $this->chunkPointers[$finalHash];
230 * Advance the chunk pointer for given final hash
232 * @param $finalHash Final hash to advance the pointer for
234 private function nextChunkPointer ($finalHash) {
235 // Is the pointer already initialized?
236 assert(isset($this->chunkPointers[$finalHash]));
239 $this->chunkPointers[$finalHash]++;
243 * "Getter" for data chunk size of given hash.
245 * @param $hash Hash to substract it's length
246 * @return $dataChunkSize The chunk size
248 private function getDataChunkSizeFromHash ($hash) {
249 // Calculate real (data) chunk size
252 ($this->chunkSize / 8) -
255 // Length of sperators
256 (strlen(self::CHUNK_DATA_HASH_SEPERATOR) * 2) -
257 // Length of max serial number
258 $this->maxSerialLength
261 // This should be larger than zero bytes
262 assert($dataChunkSize > 0);
265 return $dataChunkSize;
269 * Generates a hash from raw data
271 * @param $rawData Raw data bytes to hash
272 * @return $hash Hash from the raw data
274 private function generateHashFromRawData ($rawData) {
275 // Get the crypto instance and hash the data
276 $hash = $this->getCryptoInstance()->hashString($rawData);
283 * "Getter" for the next hexadecimal-encoded serial number
285 * @return $encodedSerialNumber The next hexadecimal-encoded serial number
287 private function getNextHexSerialNumber () {
288 // Assert on maximum serial number length
289 assert($this->serialNumber <= $this->maxSerialNumber);
291 // Encode the current serial number
292 $encodedSerialNumber = $this->dec2Hex($this->serialNumber, $this->maxSerialLength);
295 $this->serialNumber++;
297 // Return the encoded serial number
298 return $encodedSerialNumber;
302 * Appends an end-of-package chunk to the chunk list for given chunk and
305 * @param $chunkHash Last chunk's hash
306 * @param $finalHash Final hash for raw (unencoded) data
309 private function appendEndOfPackageChunk ($chunkHash, $finalHash) {
310 // Generate end-of-package marker
312 self::END_OF_PACKAGE_IDENTIFIER .
313 $finalHash . self::CHUNK_HASH_SEPERATOR .
314 $chunkHash . self::CHUNK_SEPERATOR;
316 // Also get a hash from it
317 $chunkHash = $this->generateHashFromRawData($rawData);
319 // Append it to the chunk's data and hash array
320 $this->chunkHashes[$finalHash][] = $chunkHash;
321 $this->chunks[$finalHash][] = $rawData;
325 * Splits the given encoded data into smaller chunks, the size of the final
326 * and the seperator is being subtracted from chunk size to fit it into a
327 * TCP package (512 bytes).
329 * @param $rawData Raw data string
330 * @param $finalHash Final hash from the raw data
333 private function splitEncodedDataIntoChunks ($rawData, $finalHash) {
334 // Make sure final hashes with at least 32 bytes can pass
335 assert(strlen($finalHash) >= 32);
337 // Calculate real (data) chunk size
338 $dataChunkSize = $this->getDataChunkSizeFromHash($finalHash);
339 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: dataChunkSize=' . $dataChunkSize);
345 for ($idx = 0; $idx < strlen($rawData); $idx += $dataChunkSize) {
346 // Get the next chunk
347 $chunk = substr($rawData, $idx, $dataChunkSize);
349 // Hash it and remember it in seperate array
350 $chunkHash = $this->getCryptoInstance()->hashString($chunk);
351 $this->chunkHashes[$finalHash][] = $chunkHash;
353 // Prepend the hash to the chunk
355 $chunkHash . self::CHUNK_DATA_HASH_SEPERATOR .
356 $this->getNextHexSerialNumber() . self::CHUNK_DATA_HASH_SEPERATOR .
357 $chunk . self::CHUNK_SEPERATOR
360 // Make sure the chunk is not larger than a TCP package can hold
361 assert(strlen($chunk) <= NetworkPackage::TCP_PACKAGE_SIZE);
363 // Add it to the array
364 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: Adding ' . strlen($chunk) . ' bytes of a chunk.');
365 $this->chunks[$finalHash][] = $chunk;
369 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: Raw data of ' . strlen($rawData) . ' bytes has been fragmented into ' . count($this->chunks[$finalHash]) . ' chunk(s).');
371 // Add end-of-package chunk
372 $this->appendEndOfPackageChunk($chunkHash, $finalHash);
376 * Prepends a chunk (or more) with all hashes from all chunks + final chunk.
378 * @param $rawData Raw data string
379 * @param $finalHash Final hash from the raw data
382 private function prependHashChunk ($rawData, $finalHash) {
383 // "Implode" the whole array of hashes into one string
384 $rawData = self::HASH_CHUNK_IDENTIFIER . implode(self::CHUNK_HASH_SEPERATOR, $this->chunkHashes[$finalHash]);
386 // Also get a hash from it
387 $chunkHash = $this->generateHashFromRawData($rawData);
389 // Calulcate chunk size
390 $dataChunkSize = $this->getDataChunkSizeFromHash($chunkHash);
392 // Now array_unshift() it to the two chunk arrays
393 for ($idx = 0; $idx < strlen($rawData); $idx += $dataChunkSize) {
394 // Get the next chunk
395 $chunk = substr($rawData, $idx, $dataChunkSize);
397 // Hash it and remember it in seperate array
398 $chunkHash = $this->getCryptoInstance()->hashString($chunk);
399 array_unshift($this->chunkHashes[$finalHash], $chunkHash);
401 // Prepend the hash to the chunk
403 $chunkHash . self::CHUNK_DATA_HASH_SEPERATOR .
404 $this->getNextHexSerialNumber() . self::CHUNK_DATA_HASH_SEPERATOR .
405 $chunk . self::CHUNK_SEPERATOR
408 // Make sure the chunk is not larger than a TCP package can hold
409 assert(strlen($chunk) <= NetworkPackage::TCP_PACKAGE_SIZE);
411 // Add it to the array
412 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: Adding ' . strlen($chunk) . ' bytes of a chunk.');
413 array_unshift($this->chunks[$finalHash], $chunk);
418 * This method does "implode" the given package data array into one long
419 * string, splits it into small chunks, adds a serial number and checksum
420 * to all chunks and prepends a chunk with all hashes only in it. It will
421 * return the final hash for faster processing of packages.
423 * @param $packageData Raw package data array
424 * @param $connectionInstance A helper instance for connections
425 * @return $finalHash Final hash for faster processing
426 * @todo $connectionInstance is unused
428 public function fragmentPackageArray (array $packageData, BaseConnectionHelper $connectionInstance) {
429 // Is this package already fragmented?
430 if (!$this->isPackageProcessed($packageData)) {
431 // First we need to "implode" the array
432 $rawData = implode(NetworkPackage::PACKAGE_DATA_SEPERATOR, $packageData);
434 // Generate the final hash from the raw data (not encoded!)
435 $finalHash = $this->generateHashFromRawData($rawData);
438 $this->processedPackages[$this->getProcessedPackagesIndex($packageData)] = $finalHash;
441 $this->initPointer($finalHash);
443 // Split the encoded data into smaller chunks
444 $this->splitEncodedDataIntoChunks($rawData, $finalHash);
446 // Prepend a chunk with all hashes together
447 $this->prependHashChunk($rawData, $finalHash);
449 // Mark the package as fragmented
450 $this->markPackageDataProcessed($packageData);
452 // Get the final hash from the package data
453 $finalHash = $this->getFinalHashFromPackageData($packageData);
457 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: finalHash[' . gettype($finalHash) . ']=' . $finalHash);
462 * This method gets the next chunk from the internal FIFO which should be
463 * sent to the given recipient. It will return an associative array where
464 * the key is the chunk hash and value the raw chunk data.
466 * @param $finalHash Final hash for faster lookup
467 * @return $rawDataChunk Raw package data chunk
469 public function getNextRawDataChunk ($finalHash) {
471 // Get current chunk index
472 $current = $this->getCurrentChunkPointer($finalHash);
473 } catch (AssertionException $e) {
474 // This may happen when the final hash is true
475 if ($finalHash === true) {
476 // Set current to null
479 // Throw the exception
484 // If there is no entry left, return an empty array
485 if ((!isset($this->chunkHashes[$finalHash][$current])) || (!isset($this->chunks[$finalHash][$current]))) {
486 // No more entries found
490 // Generate the array
491 $rawDataChunk = array(
492 $this->chunkHashes[$finalHash][$current] => $this->chunks[$finalHash][$current]
495 // Count one index up
496 $this->nextChunkPointer($finalHash);
498 // Return the chunk array
499 return $rawDataChunk;