* @version 0.0.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2023 Core 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 FileIoStream extends BaseFrameworkSystem implements FileInputStreamer, FileOutputStreamer { /** * File header indicator */ const FILE_IO_FILE_HEADER_ID = '@head'; /** * Data block indicator */ const FILE_IO_DATA_BLOCK_ID = '@data'; /** * Separator #1 */ const FILE_IO_CHUNKER = ':'; /** * Separator #2 */ const FILE_IO_SEPARATOR = '^'; /** * Protected constructor */ private function __construct () { // Call parent constructor parent::__construct(__CLASS__); } /** * Create a file IO stream. This is a class for performing all actions * on files like creating, deleting and loading them. * * @return $ioInstance An instance of a FileIoStream class */ public static final function createFileIoStream () { // Create new instance /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: CALLED!'); $ioInstance = new FileIoStream(); // Return the instance /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: ioInstance=%s - EXIT!', $ioInstance->__toString())); return $ioInstance; } /** * Saves data to a given local file and create missing directory structures * * @param $fileInfoInstance An instance of a SplFileInfo class * @param $dataArray The data we shall store to the file * @return void * @see FileOutputStreamer * @throws InvalidArgumentException If an invalid parameter was given * @throws OutOfBoundsException If an expected array element wasn't found */ public final function saveFile (SplFileInfo $fileInfoInstance, array $dataArray) { // Trace message /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: fileInfoInstance=%s,dataArray()=%d - CALLED!', $fileInfoInstance, count($dataArray))); if (count($dataArray) < 2) { // Not valid array, at least 2 elements must be there! throw new InvalidArgumentException(sprintf('Parameter "dataArray" should have at least 2 elements, has %d', count($dataArray))); } else if (!isset($dataArray[0])) { // Array element 0 not found throw new OutOfBoundsException(sprintf('Array element dataArray[0] not found, dataArray=%s', json_encode($dataArray))); } else if (!isset($dataArray[1])) { // Array element 1 not found throw new OutOfBoundsException(sprintf('Array element dataArray[1] not found, dataArray=%s', json_encode($dataArray))); } try { // Get a file output pointer $fileInstance = ObjectFactory::createObjectByConfiguredName('file_raw_output_class', [$fileInfoInstance, 'wb']); } catch (FileNotFoundException $e) { // Bail out ApplicationEntryPoint::exitApplication('The application has made a fatal error. Exception: ' . $e->__toString() . ' with message: ' . $e->getMessage()); } // Write a header information for validation purposes /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Writing header to fileInstance=%s ...', $fileInstance->__toString())); $fileInstance->writeToFile(sprintf('%s%s%s%s%s%s%s%s%s' . PHP_EOL, self::FILE_IO_FILE_HEADER_ID, self::FILE_IO_SEPARATOR, $dataArray[0], self::FILE_IO_CHUNKER, time(), self::FILE_IO_CHUNKER, strlen($dataArray[1]), self::FILE_IO_CHUNKER, md5($dataArray[1]) )); // Encode the (maybe) binary stream with Base64 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Encoding %d bytes to BASE64 string ...', strlen($dataArray[1]))); $b64Stream = base64_encode($dataArray[1]); // write the data line-by-line $line = str_repeat(' ', 50); $idx = 0; while (strlen($line) == 50) { // Get 50 chars or less $line = substr($b64Stream, $idx, 50); // Save it to the stream /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Writing %d bytes to file ...', strlen($line))); $fileInstance->writeToFile(sprintf('%s%s%s%s%s' . PHP_EOL, self::FILE_IO_DATA_BLOCK_ID, self::FILE_IO_SEPARATOR, $line, self::FILE_IO_CHUNKER, md5($line) )); // Advance to the next 50-chars block $idx += 50; } // Close the file /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('FILE-IO-STREAM: Closing file ...'); unset($fileInstance); // Trace message /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: EXIT!'); } /** * Reads from a local file * * @param $infoInstance An instance of a SplFileInfo class * @return $array An array with the element 'header' and 'data' * @see FileInputStreamer */ public final function loadFileContents (SplFileInfo $infoInstance) { // Initialize some variables and arrays /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: infoInstance=%s - CALLED!', $infoInstance)); $inputBuffer = ''; $lastBuffer = ''; $header = []; $data = []; $readData = ''; // This will contain our read data // Get a file input handler $fileInstance = ObjectFactory::createObjectByConfiguredName('file_raw_input_class', array($infoInstance)); // Read all it's contents (we very and transparently decompress it below) /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: fileInstance=%s', $fileInstance->__toString())); while ($readRawLine = $fileInstance->readFromFile()) { // Add the read line to the buffer /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Adding %d read bytes to input buffer.', strlen($readRawLine))); $inputBuffer .= $readRawLine; // Break infinite loop maybe caused by the input handler if ($lastBuffer == $inputBuffer) { // Break out of loop, EOF reached? /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('FILE-IO-STREAM: EOF reached!'); break; } // Remember last read line for avoiding possible infinite loops $lastBuffer = $inputBuffer; } // Close directory handle /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('FILE-IO-STREAM: Closing file ...'); unset($fileInstance); // Convert it into an array /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Read inputBuffer=%d bytes from infoInstance=%s', strlen($inputBuffer), $infoInstance)); $inputArray = explode(chr(10), $inputBuffer); // Now process the read lines and verify it's content /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: inputArray()=%d', count($inputArray))); foreach ($inputArray as $rawLine) { // Trim it a little but not the leading spaces/tab-stops $rawLine = rtrim($rawLine); // Analyze this line /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: rawLine()=%d', strlen($rawLine))); if (substr($rawLine, 0, 5) == self::FILE_IO_FILE_HEADER_ID) { // Header found, so let's extract it /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Found header, rawLine=%s', $rawLine)); $header = explode(self::FILE_IO_SEPARATOR, $rawLine); $headerLine = trim($header[1]); // Now we must convert it again into an array $header = explode(self::FILE_IO_CHUNKER, $headerLine); // Is the header (maybe) valid? /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: header()=%d', count($header))); if (count($header) != 4) { // Throw an exception throw new InvalidArrayCountException([$this, 'header', count($header), 4], self::EXCEPTION_ARRAY_HAS_INVALID_COUNT); } } elseif (substr($rawLine, 0, 5) == self::FILE_IO_DATA_BLOCK_ID) { // Is a data line! /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Data line found rawLine=%s', $rawLine)); $data = explode(self::FILE_IO_SEPARATOR, $rawLine); $dataLine = $data[1]; // First element is the data, second the MD5 checksum $data = explode(self::FILE_IO_CHUNKER, $dataLine); // Validate the read line /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: data()=%d', count($data))); if (count($data) == 2) { // Generate checksum (MD5 is okay here) $checksum = md5($data[0]); // Check if it matches provided one /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: checksum=%s,data[1]=%s', $checksum, $data[1])); if ($checksum != $data[1]) { // MD5 hash did not match! throw new InvalidMD5ChecksumException([$this, $checksum, $data[1]], self::EXCEPTION_MD5_CHECKSUMS_MISMATCH); } } else { // Invalid count! throw new InvalidArrayCountException([$this, 'data', count($data), 2], self::EXCEPTION_ARRAY_HAS_INVALID_COUNT); } // Add this to the readData string /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Adding %d raw data to input stream', strlen($data[0]))); $readData .= $data[0]; } else { // Other raw lines than header/data tagged lines and re-add the new-line char /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Adding rawLine=%s(%d) + PHP_EOL to input stream', $rawLine, strlen($rawLine))); $readData .= $rawLine . PHP_EOL; } } // Was raw lines read and no header/data? /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: readData()=%d,header()=%d,data()=%d', strlen($readData), count($header), count($data))); if ((!empty($readData)) && (count($header) == 0) && (count($data) == 0)) { // Return raw lines back /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: readData()=%d - EXIT!', strlen($readData))); return $readData; } // Was a header found? /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: header()=%d', count($header))); if (count($header) != 4) { // Throw an exception throw new InvalidArrayCountException([$this, 'header', count($header), 4], self::EXCEPTION_ARRAY_HAS_INVALID_COUNT); } // Decode all from Base64 $decodedData = @base64_decode($readData); // Does the size match? /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: decodedData()=%d,header[2]=%d', strlen($decodedData), $header[2])); if (strlen($decodedData) != $header[2]) { // Size did not match throw new InvalidDataLengthException([$this, strlen($decodedData), $header[2]], self::EXCEPTION_UNEXPECTED_STRING_SIZE); } // Generate checksum from whole read data $checksum = md5($decodedData); // Validate the decoded data with the final MD5 hash /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: checksum=%s,header[3]=%s', $checksum, $header[3])); if ($checksum != $header[3]) { // MD5 hash did not match! throw new InvalidMD5ChecksumException([$this, $checksum, $header[3]], self::EXCEPTION_MD5_CHECKSUMS_MISMATCH); } // Return all in an array /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: header()=%d,decodedData()=%d - EXIT!', count($header), strlen($decodedData))); return [ 'header' => $header, 'data' => $decodedData, ]; } /** * Streams the data and maybe does something to it * * @param $data The data (string mostly) to "stream" * @return $data The data (string mostly) to "stream" * @throws UnsupportedOperationException If this method is called */ public function streamData (string $data) { // Not supported /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: data=()=%d - CALLED!', strlen($data))); throw new UnsupportedOperationException([$this, __FUNCTION__], FrameworkInterface::EXCEPTION_UNSPPORTED_OPERATION); } /** * Determines seek position * * @return $seekPosition Current seek position * @todo 0% done */ public function determineSeekPosition () { // Trace message /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: CALLED!'); DebugMiddleware::getSelfInstance()->partialStub(); } /** * Seek to given offset (default) or other possibilities as fseek() gives. * * @param $offset Offset to seek to (or used as "base" for other seeks) * @param $whence Added to offset (default: only use offset to seek to) * @return $status Status of file seek: 0 = success, -1 = failed */ public function seek (int $offset, int $whence = SEEK_SET) { // Check parameters /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: offset=%d,whence=%d - CALLED!', $offset, $whence)); if ($offset < 0) { // Throw IAE throw new InvalidArgumentException(sprintf('offset=%d is below zero', $offset), FrameworkInterface::EXCEPTION_INVALID_ARGUMENT); } elseif ($whence < 0) { // Throw IAE throw new InvalidArgumentException(sprintf('whence=%d is below zero', $whence), FrameworkInterface::EXCEPTION_INVALID_ARGUMENT); } DebugMiddleware::getSelfInstance()->partialStub('offset=' . $offset . ',whence=' . $whence); } /** * Size of file stack * * @return $size Size (in bytes) of file */ public function size () { // Trace message /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: CALLED!'); DebugMiddleware::getSelfInstance()->partialStub(); } }