3 * A PackageFragmenter class to fragment package data into smaller chunks for
4 * delivery. This class does add a serial number to it and in the first data
5 * submission chunk it will add a sumerization of all fragements and their
8 * @author Roland Haeder <webmaster@ship-simu.org>
10 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2011 Hub Developer Team
11 * @license GNU GPL 3.0 or any newer version
12 * @link http://www.ship-simu.org
14 * This program is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with this program. If not, see <http://www.gnu.org/licenses/>.
27 class PackageFragmenter extends BaseFrameworkSystem implements Fragmentable, Registerable {
29 * Cached chunk size in bits
31 private $chunkSize = 0;
36 private $chunks = array();
39 * Array for chunk hashes
41 private $chunkHashes = array();
44 * Array for chunk pointers
46 private $chunkPointers = array();
49 * Array for processed packages
51 private $processedPackages = array();
56 private $serialNumber = 0x00000000;
59 * Length of largest possible serial number
61 private $maxSerialLength = 8;
64 * Maximum possible serial number
66 private $maxSerialNumber = 0;
68 * Seperator between chunk data, serial number and chunk hash
70 const CHUNK_DATA_HASH_SEPERATOR = '@';
73 * Seperator for all chunk hashes
75 const CHUNK_HASH_SEPERATOR = ';';
78 * Identifier for hash chunk
80 const HASH_CHUNK_IDENTIFIER = 'HASH-CHUNK:';
83 * Protected constructor
87 protected function __construct () {
88 // Call parent constructor
89 parent::__construct(__CLASS__);
91 // Init this fragmenter
92 $this->initFragmenter();
96 * Creates an instance of this class
98 * @return $fragmenterInstance An instance of a Fragmentable class
100 public static final function createPackageFragmenter () {
102 $fragmenterInstance = new PackageFragmenter();
104 // Get an output stream for all out-going packages
105 $streamInstance = ObjectFactory::createObjectByConfiguredName('node_raw_package_output_stream');
107 // And set it in this fragmenter
108 $fragmenterInstance->setOutputStreamInstance($streamInstance);
110 // And also a crypto instance (for our encrypted messages)
111 $cryptoInstance = ObjectFactory::createObjectByConfiguredName('crypto_class');
112 $fragmenterInstance->setCryptoInstance($cryptoInstance);
114 // Return the prepared instance
115 return $fragmenterInstance;
119 * Initializes this fragmenter
123 private function initFragmenter () {
124 // Load some configuration entries and "cache" them:
125 // - Chunk size in bits
126 $this->chunkSize = $this->getConfigInstance()->getConfigEntry('package_chunk_size');
128 // - Maximum serial number
129 $this->maxSerialNumber = $this->hex2dec(str_repeat('f', $this->maxSerialLength));
133 * Initializes the pointer for given final hash
135 * @param $finalHash Final hash to initialize pointer for
138 private function initPointer ($finalHash) {
139 $this->chunkPointers[$finalHash] = 0;
143 * "Getter" for processedPackages array index
145 * @param $packageData Raw package data array
146 * @return $index Array index for processedPackages
148 private function getProcessedPackagesIndex (array $packageData) {
150 $packageData['sender'] . NetworkPackage::PACKAGE_DATA_SEPERATOR .
151 $packageData['recipient'] . NetworkPackage::PACKAGE_DATA_SEPERATOR .
152 $packageData['content'] . NetworkPackage::PACKAGE_DATA_SEPERATOR
157 * Checks wether the given package data is already processed by this fragmenter
159 * @param $packageData Raw package data array
160 * @return $isProcessed Wether the package has been fragmented
162 private function isPackageProcessed (array $packageData) {
164 $index = $this->getProcessedPackagesIndex($packageData);
166 // Is the array index there?
168 (isset($this->processedPackages[$index])) &&
169 ($this->processedPackages[$index] === true)
177 * Marks the given package data as processed by this fragmenter
179 * @param $packageData Raw package data array
182 private function markPackageDataProcessed (array $packageData) {
183 // Remember it (until we may remove it)
184 $this->processedPackages[$this->getProcessedPackagesIndex($packageData)] = true;
188 * Getter for final hash from given package data
190 * @param $packageData Raw package data array
191 * @return $finalHash Final hash for package data
193 private function getFinalHashFromPackageData (array $packageData) {
194 // Make sure it is there
195 assert(isset($this->processedPackages[$this->getProcessedPackagesIndex($packageData)]));
198 return $this->processedPackages[$this->getProcessedPackagesIndex($packageData)];
202 * Get next chunk pointer for given final hash
204 * @param $finalHash Final hash to get current pointer for
206 private function getCurrentChunkPointer ($finalHash) {
207 // Is the final hash valid?
208 assert(strlen($finalHash) > 0);
210 // Is the pointer already initialized?
211 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: finalHash=' . $finalHash);
212 assert(isset($this->chunkPointers[$finalHash]));
215 return $this->chunkPointers[$finalHash];
219 * Advance the chunk pointer for given final hash
221 * @param $finalHash Final hash to advance the pointer for
223 private function nextChunkPointer ($finalHash) {
224 // Is the pointer already initialized?
225 assert(isset($this->chunkPointers[$finalHash]));
228 $this->chunkPointers[$finalHash]++;
232 * "Getter" for data chunk size of given hash.
234 * @param $hash Hash to substract it's length
235 * @return $dataChunkSize The chunk size
237 private function getDataChunkSizeFromHash ($hash) {
238 // Calculate real (data) chunk size
241 ($this->chunkSize / 8) -
244 // Length of sperators
245 (strlen(self::CHUNK_DATA_HASH_SEPERATOR) * 2) -
246 // Length of max serial number
247 $this->maxSerialLength
250 // This should be larger than zero bytes
251 assert($dataChunkSize > 0);
254 return $dataChunkSize;
258 * Generates a hash from raw data
260 * @param $rawData Raw data bytes to hash
261 * @return $hash Hash from the raw data
263 private function generateHashFromRawData ($rawData) {
264 // Get the crypto instance and hash the data
265 $hash = $this->getCryptoInstance()->hashString($rawData);
272 * "Getter" for the next hexadecimal-encoded serial number
274 * @return $encodedSerialNumber The next hexadecimal-encoded serial number
276 private function getNextHexSerialNumber () {
277 // Assert on maximum serial number length
278 assert($this->serialNumber <= $this->maxSerialNumber);
280 // Encode the current serial number
281 $encodedSerialNumber = $this->dec2Hex($this->serialNumber, $this->maxSerialLength);
284 $this->serialNumber++;
286 // Return the encoded serial number
287 return $encodedSerialNumber;
291 * Splits the given encoded data into smaller chunks, the size of the final
292 * and the seperator is being subtracted from chunk size to fit it into a
293 * TCP package (512 bytes).
295 * @param $encodedData Encoded data string
296 * @param $finalHash Final hash from the raw data
299 private function splitEncodedDataIntoChunks ($encodedData, $finalHash) {
300 // Make sure final hashes with at least 32 bytes can pass
301 assert(strlen($finalHash) >= 32);
303 // Calculate real (data) chunk size
304 $dataChunkSize = $this->getDataChunkSizeFromHash($finalHash);
305 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: dataChunkSize=' . $dataChunkSize);
308 for ($idx = 0; $idx < strlen($encodedData); $idx += $dataChunkSize) {
309 // Get the next chunk
310 $chunk = substr($encodedData, $idx, $dataChunkSize);
312 // Hash it and remember it in seperate array
313 $chunkHash = $this->getCryptoInstance()->hashString($chunk);
314 $this->chunkHashes[$finalHash][] = $chunkHash;
316 // Prepend the hash to the chunk
317 $chunk = $chunkHash . self::CHUNK_DATA_HASH_SEPERATOR . $this->getNextHexSerialNumber() . self::CHUNK_DATA_HASH_SEPERATOR . $chunk;
319 // Make sure the chunk is not larger than a TCP package can hold
320 assert(strlen($chunk) <= NetworkPackage::TCP_PACKAGE_SIZE);
322 // Add it to the array
323 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: Adding ' . strlen($chunk) . ' bytes of a chunk.');
324 $this->chunks[$finalHash][] = $chunk;
328 $this->debugOutput('FRAGMENTER: Encoded data of ' . strlen($encodedData) . ' bytes has been fragmented into ' . count($this->chunks[$finalHash]) . ' chunk(s).');
332 * Prepends a chunk (or more) with all hashes from all chunks + final chunk.
334 * @param $encodedData Encoded data string
335 * @param $finalHash Final hash from the raw data
338 private function prependHashChunk ($encodedData, $finalHash) {
339 // "Implode" the whole array of hashes into one string
340 $rawData = self::HASH_CHUNK_IDENTIFIER . implode(self::CHUNK_HASH_SEPERATOR, $this->chunkHashes[$finalHash]);
342 // Also get a hash from it
343 $chunkHash = $this->generateHashFromRawData($rawData);
345 // Also encode this one
346 $encodedData = $this->getOutputStreamInstance()->streamData($rawData);
348 // Calulcate chunk size
349 $dataChunkSize = $this->getDataChunkSizeFromHash($chunkHash);
351 // Now array_unshift() it to the two chunk arrays
352 for ($idx = 0; $idx < strlen($encodedData); $idx += $dataChunkSize) {
353 // Get the next chunk
354 $chunk = substr($encodedData, $idx, $dataChunkSize);
356 // Hash it and remember it in seperate array
357 $chunkHash = $this->getCryptoInstance()->hashString($chunk);
358 array_unshift($this->chunkHashes[$finalHash], $chunkHash);
360 // Prepend the hash to the chunk
361 $chunk = $chunkHash . self::CHUNK_DATA_HASH_SEPERATOR . $this->getNextHexSerialNumber() . self::CHUNK_DATA_HASH_SEPERATOR . $chunk;
363 // Make sure the chunk is not larger than a TCP package can hold
364 assert(strlen($chunk) <= NetworkPackage::TCP_PACKAGE_SIZE);
366 // Add it to the array
367 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: Adding ' . strlen($chunk) . ' bytes of a chunk.');
368 array_unshift($this->chunks[$finalHash], $chunk);
373 * This method does "implode" the given package data array into one long
374 * string, splits it into small chunks, adds a serial number and checksum
375 * to all chunks and prepends a chunk with all hashes only in it. It will
376 * return the final hash for faster processing of packages.
378 * @param $packageData Raw package data array
379 * @param $connectionInstance A helper instance for connections
380 * @return $finalHash Final hash for faster processing
381 * @todo $connectionInstance is unused
383 public function fragmentPackageArray (array $packageData, BaseConnectionHelper $connectionInstance) {
384 // Is this package already fragmented?
385 if (!$this->isPackageProcessed($packageData)) {
386 // First we need to "implode" the array
387 $rawData = implode(NetworkPackage::PACKAGE_DATA_SEPERATOR, $packageData);
389 // Generate the final hash from the raw data (not encoded!)
390 $finalHash = $this->generateHashFromRawData($rawData);
393 $this->processedPackages[$this->getProcessedPackagesIndex($packageData)] = $finalHash;
396 $this->initPointer($finalHash);
398 // Encode the package for delivery
399 $encodedData = $this->getOutputStreamInstance()->streamData($rawData);
401 // Split the encoded data into smaller chunks
402 $this->splitEncodedDataIntoChunks($encodedData, $finalHash);
404 // Prepend a chunk with all hashes together
405 $this->prependHashChunk($encodedData, $finalHash);
407 // Mark the package as fragmented
408 $this->markPackageDataProcessed($packageData);
410 // Get the final hash from the package data
411 $finalHash = $this->getFinalHashFromPackageData($packageData);
415 //* NOISY-DEBUG: */ $this->debugOutput('FRAGMENTER: finalHash[' . gettype($finalHash) . ']=' . $finalHash);
420 * This method gets the next chunk from the internal FIFO which should be
421 * sent to the given recipient. It will return an associative array where
422 * the key is the chunk hash and value the raw chunk data.
424 * @param $finalHash Final hash for faster lookup
425 * @return $rawDataChunk Raw package data chunk
427 public function getNextRawDataChunk ($finalHash) {
429 // Get current chunk index
430 $current = $this->getCurrentChunkPointer($finalHash);
431 } catch (AssertionException $e) {
432 // This may happen when the final hash is true
433 if ($finalHash === true) {
434 // Set current to null
437 // Throw the exception
442 // If there is no entry left, return an empty array
443 if ((!isset($this->chunkHashes[$finalHash][$current])) || (!isset($this->chunks[$finalHash][$current]))) {
444 // No more entries found
448 // Generate the array
449 $rawDataChunk = array(
450 $this->chunkHashes[$finalHash][$current] => $this->chunks[$finalHash][$current]
453 // Count one index up
454 $this->nextChunkPointer($finalHash);
456 // Return the chunk array
457 return $rawDataChunk;