]> git.mxchange.org Git - hub.git/blob - application/hub/classes/package/fragmenter/class_PackageFragmenter.php
Updated 'core' + renamed 'main' -> 'classes'.
[hub.git] / application / hub / classes / package / fragmenter / class_PackageFragmenter.php
1 <?php
2 /**
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".
7  *
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.
13  *
14  * @author              Roland Haeder <webmaster@shipsimu.org>
15  * @version             0.0.0
16  * @copyright   Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2015 Hub Developer Team
17  * @license             GNU GPL 3.0 or any newer version
18  * @link                http://www.shipsimu.org
19  *
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.
24  *
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.
29  *
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/>.
32  */
33 class PackageFragmenter extends BaseHubSystem implements Fragmentable, Registerable {
34         /**
35          * Cached chunk size in bits
36          */
37         private $chunkSize = 0;
38
39         /**
40          * Array for chunks
41          */
42         private $chunks = array();
43
44         /**
45          * Array for chunk hashes
46          */
47         private $chunkHashes = array();
48
49         /**
50          * Array for chunk pointers
51          */
52         private $chunkPointers = array();
53
54         /**
55          * Array for processed packages
56          */
57         private $processedPackages = array();
58
59         /**
60          * Serial numbers (array key is final hash)
61          */
62         private $serialNumber = array();
63
64         /**
65          * Maximum possible serial number, "cache" for speeding up things
66          */
67         private $maxSerialNumber  = 0;
68
69         /**
70          * Length of largest possible serial number
71          */
72         const MAX_SERIAL_LENGTH = 8;
73
74         /**
75          * Separator between chunk data, serial number and chunk hash
76          */
77         const CHUNK_DATA_HASH_SEPARATOR = '@';
78
79         /**
80          * SEPARATOR for all chunk hashes
81          */
82         const CHUNK_HASH_SEPARATOR = ';';
83
84         /**
85          * SEPARATOR between two chunks
86          */
87         const CHUNK_SEPARATOR = '|';
88
89         /**
90          * Identifier for hash chunk
91          */
92         const HASH_CHUNK_IDENTIFIER = 'HASH-CHUNK:';
93
94         /**
95          * Identifier for end-of-package marker
96          */
97         const END_OF_PACKAGE_IDENTIFIER = 'EOP:';
98
99         /**
100          * Protected constructor
101          *
102          * @return      void
103          */
104         protected function __construct () {
105                 // Call parent constructor
106                 parent::__construct(__CLASS__);
107
108                 // Init this fragmenter
109                 $this->initFragmenter();
110         }
111
112         /**
113          * Creates an instance of this class
114          *
115          * @return      $fragmenterInstance             An instance of a Fragmentable class
116          */
117         public static final function createPackageFragmenter () {
118                 // Get new instance
119                 $fragmenterInstance = new PackageFragmenter();
120
121                 // Get a crypto instance and set it here
122                 $cryptoInstance = ObjectFactory::createObjectByConfiguredName('crypto_class');
123                 $fragmenterInstance->setCryptoInstance($cryptoInstance);
124
125                 // Return the prepared instance
126                 return $fragmenterInstance;
127         }
128
129         /**
130          * Initializes this fragmenter
131          *
132          * @return      void
133          */
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');
138
139                 // - Maximum serial number
140                 $this->maxSerialNumber = $this->hex2dec(str_repeat('f', self::MAX_SERIAL_LENGTH));
141         }
142
143         /**
144          * Initializes the pointer for given final hash
145          *
146          * @param       $finalHash      Final hash to initialize pointer for
147          * @return      void
148          */
149         private function initPointer ($finalHash) {
150                 $this->chunkPointers[$finalHash] = 0;
151         }
152
153         /**
154          * "Getter" for processedPackages array index
155          *
156          * @param       $packageData    Raw package data array
157          * @return      $index                  Array index for processedPackages
158          */
159         private function getProcessedPackagesIndex (array $packageData) {
160                 return (
161                         $packageData[NetworkPackage::PACKAGE_DATA_SENDER]    . NetworkPackage::PACKAGE_DATA_SEPARATOR .
162                         $packageData[NetworkPackage::PACKAGE_DATA_RECIPIENT] . NetworkPackage::PACKAGE_DATA_SEPARATOR .
163                         $packageData[NetworkPackage::PACKAGE_DATA_CONTENT]   . NetworkPackage::PACKAGE_DATA_SEPARATOR
164                 );
165         }
166
167         /**
168          * Checks whether the given package data is already processed by this fragmenter
169          *
170          * @param       $packageData    Raw package data array
171          * @return      $isProcessed    Whether the package has been fragmented
172          */
173         private function isPackageProcessed (array $packageData) {
174                 // Get array index
175                 $index = $this->getProcessedPackagesIndex($packageData);
176
177                 // Is the array index there?
178                 $isProcessed = (
179                         (isset($this->processedPackages[$index]))
180                                 &&
181                         ($this->processedPackages[$index] === TRUE)
182                 );
183
184                 // Return it
185                 return $isProcessed;
186         }
187
188         /**
189          * Marks the given package data as processed by this fragmenter
190          *
191          * @param       $packageData    Raw package data array
192          * @return      void
193          */
194         private function markPackageDataProcessed (array $packageData) {
195                 // Remember it (until we may remove it)
196                 $this->processedPackages[$this->getProcessedPackagesIndex($packageData)] = TRUE;
197         }
198
199         /**
200          * Getter for final hash from given package data
201          *
202          * @param       $packageData    Raw package data array
203          * @return      $finalHash              Final hash for package data
204          */
205         private function getFinalHashFromPackageData (array $packageData) {
206                 // Make sure it is there
207                 assert(isset($this->processedPackages[$this->getProcessedPackagesIndex($packageData)]));
208
209                 // Return it
210                 return $this->processedPackages[$this->getProcessedPackagesIndex($packageData)];
211         }
212
213         /**
214          * Get next chunk pointer for given final hash
215          *
216          * @param       $finalHash      Final hash to get current pointer for
217          */
218         private function getCurrentChunkPointer ($finalHash) {
219                 // Is the final hash valid?
220                 assert(strlen($finalHash) > 0);
221
222                 // Is the pointer already initialized?
223                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: finalHash[' . gettype($finalHash) . ']=' . $finalHash);
224                 assert(isset($this->chunkPointers[$finalHash]));
225
226                 // Return it
227                 return $this->chunkPointers[$finalHash];
228         }
229
230         /**
231          * Advance the chunk pointer for given final hash
232          *
233          * @param       $finalHash      Final hash to advance the pointer for
234          */
235         private function nextChunkPointer ($finalHash) {
236                 // Is the pointer already initialized?
237                 assert(isset($this->chunkPointers[$finalHash]));
238
239                 // Count one up
240                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: finalHash[' . gettype($finalHash) . ']=' . $finalHash);
241                 $this->chunkPointers[$finalHash]++;
242         }
243
244         /**
245          * "Getter" for data chunk size of given hash.
246          *
247          * @param       $hash                   Hash to substract it's length
248          * @return      $dataChunkSize  The chunk size
249          */
250         private function getDataChunkSizeFromHash ($hash) {
251                 // Calculate real (data) chunk size
252                 $dataChunkSize = (
253                         // Real chunk size
254                         ($this->chunkSize / 8) -
255                         // Hash size
256                         strlen($hash) -
257                         // Length of sperators
258                         (strlen(self::CHUNK_DATA_HASH_SEPARATOR) * 2) -
259                         // Length of max serial number
260                         self::MAX_SERIAL_LENGTH
261                 );
262
263                 // This should be larger than zero bytes
264                 assert($dataChunkSize > 0);
265
266                 // Return it
267                 return $dataChunkSize;
268         }
269
270         /**
271          * Generates a hash from raw data
272          *
273          * @param       $rawData        Raw data bytes to hash
274          * @return      $hash           Hash from the raw data
275          * @todo        Implement a way to send non-announcement packages with extra-salt
276          */
277         private function generateHashFromRawData ($rawData) {
278                 /*
279                  * Get the crypto instance and hash the data with no extra salt because
280                  * the other peer doesn't have *this* peer's salt.
281                  */
282                 $hash = $this->getCryptoInstance()->hashString($rawData, '', FALSE);
283
284                 // Return it
285                 return $hash;
286         }
287
288         /**
289          * Appends an end-of-package chunk to the chunk list for given chunk and
290          * final hash. As of 23-March-2012 the format of this chunk will be as any
291          * regular one to keep things easy (KISS) in ChunkHandler class.
292          *
293          * @param       $lastChunk      Last chunk raw data
294          * @param       $finalHash      Final hash for raw (unencoded) data
295          * @return      void
296          */
297         private function appendEndOfPackageChunk ($lastChunk, $finalHash) {
298                 // Generate end-of-package marker
299                 $chunkData =
300                         self::END_OF_PACKAGE_IDENTIFIER .
301                         $finalHash . self::CHUNK_HASH_SEPARATOR .
302                         $this->generateHashFromRawData($lastChunk);
303
304                 // Debug message
305                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: Adding EOP chunk with size of ' . strlen($chunkData) . ',finalHash=' . $finalHash . ' ...');
306
307                 // Add it as regular chunk
308                 $this->addChunkData($finalHash, $chunkData);
309         }
310
311         /**
312          * Splits the given encoded data into smaller chunks, the size of the final
313          * and the SEPARATOR is being subtracted from chunk size to fit it into a
314          * TCP package (512 bytes).
315          *
316          * @param       $rawData        Raw data string
317          * @param       $finalHash      Final hash from the raw data
318          * @return      void
319          */
320         private function splitEncodedDataIntoChunks ($rawData, $finalHash) {
321                 // Make sure final hashes with at least 32 bytes can pass
322                 assert(strlen($finalHash) >= 32);
323
324                 // Calculate real (data) chunk size
325                 $dataChunkSize = $this->getDataChunkSizeFromHash($finalHash);
326                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: dataChunkSize=' . $dataChunkSize);
327
328                 // Init variables
329                 $chunkHash = '';
330                 $chunkData = '';
331
332                 // Now split it up
333                 for ($idx = 0; $idx < strlen($rawData); $idx += $dataChunkSize) {
334                         // Get the next chunk
335                         $chunkData = substr($rawData, $idx, $dataChunkSize);
336
337                         // Add the chunk to the propper array and do all the stuff there
338                         $this->addChunkData($finalHash, $chunkData);
339                 } // END - for
340
341                 // Debug output
342                 //* 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).');
343
344                 // Add end-of-package chunk
345                 $this->appendEndOfPackageChunk($chunkData, $finalHash);
346         }
347
348         /**
349          * Adds the given chunk (raw data) to the proper array and hashes it for
350          * later verfication.
351          *
352          * @param       $finalHash      Final hash for faster processing
353          * @param       $chunkData      Raw chunk data
354          * @param       $prepend        Whether append (default) or prepend the chunk
355          * @return      void
356          */
357         private function addChunkData ($finalHash, $chunkData, $prepend = FALSE) {
358                 // Hash it
359                 $rawDataHash = $this->getCryptoInstance()->hashString($chunkData, '', FALSE);
360
361                 // Prepend the hash to the chunk
362                 $rawData = (
363                         $rawDataHash . self::CHUNK_DATA_HASH_SEPARATOR .
364                         $this->getNextHexSerialNumber($finalHash) . self::CHUNK_DATA_HASH_SEPARATOR .
365                         $chunkData . self::CHUNK_SEPARATOR
366                 );
367
368                 // Make sure the chunk is not larger than a TCP package can hold
369                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: assert: ' . strlen($rawData) . '/' . NetworkPackage::TCP_PACKAGE_SIZE . ' ...');
370                 // @TODO This assert broke packages where the hash chunk was very large: assert(strlen($rawData) <= NetworkPackage::TCP_PACKAGE_SIZE);
371
372                 // Add it to the array
373                 if ($prepend === TRUE) {
374                         // Debug message
375                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: Prepending ' . strlen($rawData) . ' bytes of a chunk, finalHash=' . $finalHash . ' ...');
376                         array_unshift($this->chunkHashes[$finalHash], $rawDataHash);
377                         array_unshift($this->chunks[$finalHash]     , $rawData);
378                 } else {
379                         // Debug message
380                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: Appending ' . strlen($rawData) . ' bytes of a chunk, finalHash=' . $finalHash . ' ...');
381
382                         // Is the array there?
383                         if (!isset($this->chunks[$finalHash])) {
384                                 // Then initialize it
385                                 $this->chunks[$finalHash]      = array();
386                                 $this->chunkHashes[$finalHash] = array();
387                         } // END - if
388
389                         // Add both
390                         array_push($this->chunks[$finalHash]     , $rawData);
391                         array_push($this->chunkHashes[$finalHash], $rawDataHash);
392                 }
393         }
394
395         /**
396          * Prepends a chunk (or more) with all hashes from all chunks + final chunk.
397          *
398          * @param       $finalHash      Final hash from the raw data
399          * @return      void
400          */
401         private function prependHashChunk ($finalHash) {
402                 // "Implode" the whole array of hashes into one string
403                 $rawData = self::HASH_CHUNK_IDENTIFIER . implode(self::CHUNK_HASH_SEPARATOR, $this->chunkHashes[$finalHash]);
404
405                 // Prepend chunk
406                 $this->addChunkData($finalHash, $rawData, TRUE);
407         }
408
409         /**
410          * "Getter" for the next hexadecimal-encoded serial number
411          *
412          * @param       $finalHash                              Final hash
413          * @return      $encodedSerialNumber    The next hexadecimal-encoded serial number
414          */
415         public function getNextHexSerialNumber ($finalHash) {
416                 // Assert on maximum serial number length
417                 assert(isset($this->serialNumber[$finalHash]));
418                 assert($this->serialNumber[$finalHash] <= $this->maxSerialNumber);
419
420                 // Encode the current serial number
421                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: serialNumber[' . $finalHash . ']=' . $this->serialNumber[$finalHash]);
422                 $encodedSerialNumber = $this->dec2Hex($this->serialNumber[$finalHash], self::MAX_SERIAL_LENGTH);
423
424                 // Count one up
425                 $this->serialNumber[$finalHash]++;
426
427                 // Return the encoded serial number
428                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: encodedSerialNumber=' . $encodedSerialNumber);
429                 return $encodedSerialNumber;
430         }
431
432         /**
433          * This method does "implode" the given package data array into one long
434          * string, splits it into small chunks, adds a serial number and checksum
435          * to all chunks and prepends a chunk with all hashes only in it. It will
436          * return the final hash for faster processing of packages.
437          *
438          * @param       $packageData            Raw package data array
439          * @param       $helperInstance         An instance of a ConnectionHelper class
440          * @return      $finalHash                      Final hash for faster processing
441          * @todo        $helperInstance is unused
442          */
443         public function fragmentPackageArray (array $packageData, ConnectionHelper $helperInstance) {
444                 // Is this package already fragmented?
445                 if (!$this->isPackageProcessed($packageData)) {
446                         // First we need to "implode" the array
447                         $rawData = implode(NetworkPackage::PACKAGE_DATA_SEPARATOR, $packageData);
448
449                         // Generate the final hash from the raw data (not encoded!)
450                         $finalHash = $this->generateHashFromRawData($rawData);
451
452                         // Remember it
453                         $this->processedPackages[$this->getProcessedPackagesIndex($packageData)] = $finalHash;
454
455                         // Init pointer and reset serial number
456                         $this->initPointer($finalHash);
457                         $this->resetSerialNumber($finalHash);
458
459                         // Split the encoded data into smaller chunks
460                         $this->splitEncodedDataIntoChunks($rawData, $finalHash);
461
462                         // Prepend a chunk with all hashes together
463                         $this->prependHashChunk($finalHash);
464
465                         // Mark the package as fragmented
466                         $this->markPackageDataProcessed($packageData);
467                 } else {
468                         // Get the final hash from the package data
469                         $finalHash = $this->getFinalHashFromPackageData($packageData);
470                 }
471
472                 // Return final hash
473                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: finalHash[' . gettype($finalHash) . ']=' . $finalHash);
474                 return $finalHash;
475         }
476
477         /**
478          * This method gets the next chunk from the internal FIFO which should be
479          * sent to the given recipient. It will return an associative array where
480          * the key is the chunk hash and value the raw chunk data.
481          *
482          * @param       $finalHash              Final hash for faster lookup
483          * @return      $rawDataChunk   Raw package data chunk
484          * @throws      AssertionException      If $finalHash was not 'TRUE'
485          */
486         public function getNextRawDataChunk ($finalHash) {
487                 // Debug message
488                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput('PACKAGE-FRAGMENTER[' . __METHOD__ . ':' . __LINE__ . ']: finalHash[' . gettype($finalHash) . ']=' . $finalHash);
489
490                 try {
491                         // Get current chunk index
492                         $current = $this->getCurrentChunkPointer($finalHash);
493                 } catch (AssertionException $e) {
494                         // This may happen when the final hash is TRUE
495                         if ($finalHash === TRUE) {
496                                 // Set current to null
497                                 $current = NULL;
498                         } else {
499                                 // Throw the exception
500                                 throw $e;
501                         }
502                 }
503
504                 // If there is no entry left, return an empty array
505                 if ((!isset($this->chunkHashes[$finalHash][$current])) || (!isset($this->chunks[$finalHash][$current]))) {
506                         // No more entries found
507                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput(__METHOD__. ': finalHash=' . $finalHash . ',current=' . $current . ' - No more entries found!');
508                         return array();
509                 } // END - if
510
511                 // Debug message
512                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__)->debugOutput(__METHOD__. ': finalHash=' . $finalHash . ',current=' . $current . ',chunkHashes()=' . count($this->chunkHashes[$finalHash]) .' - Entry choosen ...');
513
514                 // Generate the array
515                 $rawDataChunk = array(
516                         $this->chunkHashes[$finalHash][$current] => $this->chunks[$finalHash][$current]
517                 );
518
519                 // Count one index up
520                 $this->nextChunkPointer($finalHash);
521
522                 // Return the chunk array
523                 return $rawDataChunk;
524         }
525
526         /**
527          * Resets the serial number to zero for given final hash
528          *
529          * @param       $finalHash      Final hash to reset counter for
530          * @return      void
531          */
532         public function resetSerialNumber ($finalHash) {
533                 // Final hash must be set
534                 assert((is_string($finalHash)) && (!empty($finalHash)));
535
536                 // Reset/set serial number
537                 $this->serialNumber[$finalHash] = 0;
538         }
539 }
540
541 // [EOF]
542 ?>