Continued:
[core.git] / framework / main / classes / file_directories / io_stream / class_FileIoStream.php
1 <?php
2 // Own namespace
3 namespace Org\Mxchange\CoreFramework\Stream\Filesystem;
4
5 // Import framework stuff
6 use Org\Mxchange\CoreFramework\EntryPoint\ApplicationEntryPoint;
7 use Org\Mxchange\CoreFramework\Factory\Object\ObjectFactory;
8 use Org\Mxchange\CoreFramework\Filesystem\FileNotFoundException;
9 use Org\Mxchange\CoreFramework\Generic\FrameworkInterface;
10 use Org\Mxchange\CoreFramework\Generic\UnsupportedOperationException;
11 use Org\Mxchange\CoreFramework\Middleware\Debug\DebugMiddleware;
12 use Org\Mxchange\CoreFramework\Object\BaseFrameworkSystem;
13 use Org\Mxchange\CoreFramework\Stream\Filesystem\FileInputStreamer;
14 use Org\Mxchange\CoreFramework\Stream\Filesystem\FileOutputStreamer;
15
16 // Import SPL stuff
17 use \InvalidArgumentException;
18 use \OutOfBoundsException;
19 use \SplFileInfo;
20
21 /**
22  * An universal class for file input/output streams.
23  *
24  * @author              Roland Haeder <webmaster@shipsimu.org>
25  * @version             0.0.0
26  * @copyright   Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2023 Core Developer Team
27  * @license             GNU GPL 3.0 or any newer version
28  * @link                http://www.shipsimu.org
29  *
30  * This program is free software: you can redistribute it and/or modify
31  * it under the terms of the GNU General Public License as published by
32  * the Free Software Foundation, either version 3 of the License, or
33  * (at your option) any later version.
34  *
35  * This program is distributed in the hope that it will be useful,
36  * but WITHOUT ANY WARRANTY; without even the implied warranty of
37  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
38  * GNU General Public License for more details.
39  *
40  * You should have received a copy of the GNU General Public License
41  * along with this program. If not, see <http://www.gnu.org/licenses/>.
42  */
43 class FileIoStream extends BaseFrameworkSystem implements FileInputStreamer, FileOutputStreamer {
44         /**
45          * File header indicator
46          */
47         const FILE_IO_FILE_HEADER_ID = '@head';
48
49         /**
50          * Data block indicator
51          */
52         const FILE_IO_DATA_BLOCK_ID = '@data';
53
54         /**
55          * Separator #1
56          */
57         const FILE_IO_CHUNKER = ':';
58
59         /**
60          * Separator #2
61          */
62         const FILE_IO_SEPARATOR = '^';
63
64         /**
65          * Protected constructor
66          */
67         private function __construct () {
68                 // Call parent constructor
69                 parent::__construct(__CLASS__);
70         }
71
72         /**
73          * Create a file IO stream. This is a class for performing all actions
74          * on files like creating, deleting and loading them.
75          *
76          * @return      $ioInstance     An instance of a FileIoStream class
77          */
78         public static final function createFileIoStream () {
79                 // Create new instance
80                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: CALLED!');
81                 $ioInstance = new FileIoStream();
82
83                 // Return the instance
84                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: ioInstance=%s - EXIT!', $ioInstance->__toString()));
85                 return $ioInstance;
86         }
87
88         /**
89          * Saves data to a given local file and create missing directory structures
90          *
91          * @param       $fileInfoInstance       An instance of a SplFileInfo class
92          * @param       $dataArray      The data we shall store to the file
93          * @return      void
94          * @see         FileOutputStreamer
95          * @throws      InvalidArgumentException        If an invalid parameter was given
96          * @throws      OutOfBoundsException    If an expected array element wasn't found
97          */
98         public final function saveFile (SplFileInfo $fileInfoInstance, array $dataArray) {
99                 // Trace message
100                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: fileInfoInstance=%s,dataArray()=%d - CALLED!', $fileInfoInstance, count($dataArray)));
101                 if (count($dataArray) < 2) {
102                         // Not valid array, at least 2 elements must be there!
103                         throw new InvalidArgumentException(sprintf('Parameter "dataArray" should have at least 2 elements, has %d', count($dataArray)));
104                 } else if (!isset($dataArray[0])) {
105                         // Array element 0 not found
106                         throw new OutOfBoundsException(sprintf('Array element dataArray[0] not found, dataArray=%s', json_encode($dataArray)));
107                 } else if (!isset($dataArray[1])) {
108                         // Array element 1 not found
109                         throw new OutOfBoundsException(sprintf('Array element dataArray[1] not found, dataArray=%s', json_encode($dataArray)));
110                 }
111
112                 try {
113                         // Get a file output pointer
114                         $fileInstance = ObjectFactory::createObjectByConfiguredName('file_raw_output_class', [$fileInfoInstance, 'wb']);
115                 } catch (FileNotFoundException $e) {
116                         // Bail out
117                         ApplicationEntryPoint::exitApplication('The application has made a fatal error. Exception: ' . $e->__toString() . ' with message: ' . $e->getMessage());
118                 }
119
120                 // Write a header information for validation purposes
121                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Writing header to fileInstance=%s ...', $fileInstance->__toString()));
122                 $fileInstance->writeToFile(sprintf('%s%s%s%s%s%s%s%s%s' . PHP_EOL,
123                         self::FILE_IO_FILE_HEADER_ID,
124                         self::FILE_IO_SEPARATOR,
125                         $dataArray[0],
126                         self::FILE_IO_CHUNKER,
127                         time(),
128                         self::FILE_IO_CHUNKER,
129                         strlen($dataArray[1]),
130                         self::FILE_IO_CHUNKER,
131                         md5($dataArray[1])
132                 ));
133
134                 // Encode the (maybe) binary stream with Base64
135                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Encoding %d bytes to BASE64 string ...', strlen($dataArray[1])));
136                 $b64Stream = base64_encode($dataArray[1]);
137
138                 // write the data line-by-line
139                 $line = str_repeat(' ', 50); $idx = 0;
140                 while (strlen($line) == 50) {
141                         // Get 50 chars or less
142                         $line = substr($b64Stream, $idx, 50);
143
144                         // Save it to the stream
145                         /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Writing %d bytes to file ...', strlen($line)));
146                         $fileInstance->writeToFile(sprintf('%s%s%s%s%s' . PHP_EOL,
147                                 self::FILE_IO_DATA_BLOCK_ID,
148                                 self::FILE_IO_SEPARATOR,
149                                 $line,
150                                 self::FILE_IO_CHUNKER,
151                                 md5($line)
152                         ));
153
154                         // Advance to the next 50-chars block
155                         $idx += 50;
156                 }
157
158                 // Close the file
159                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('FILE-IO-STREAM: Closing file ...');
160                 unset($fileInstance);
161
162                 // Trace message
163                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: EXIT!');
164         }
165
166         /**
167          * Reads from a local file
168          *
169          * @param       $infoInstance   An instance of a SplFileInfo class
170          * @return      $array  An array with the element 'header' and 'data'
171          * @see         FileInputStreamer
172          */
173         public final function loadFileContents (SplFileInfo $infoInstance) {
174                 // Initialize some variables and arrays
175                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: infoInstance=%s - CALLED!', $infoInstance));
176                 $inputBuffer = '';
177                 $lastBuffer = '';
178                 $header = [];
179                 $data = [];
180                 $readData = ''; // This will contain our read data
181
182                 // Get a file input handler
183                 $fileInstance = ObjectFactory::createObjectByConfiguredName('file_raw_input_class', array($infoInstance));
184
185                 // Read all it's contents (we very and transparently decompress it below)
186                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: fileInstance=%s', $fileInstance->__toString()));
187                 while ($readRawLine = $fileInstance->readFromFile()) {
188                         // Add the read line to the buffer
189                         /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Adding %d read bytes to input buffer.', strlen($readRawLine)));
190                         $inputBuffer .= $readRawLine;
191
192                         // Break infinite loop maybe caused by the input handler
193                         if ($lastBuffer == $inputBuffer) {
194                                 // Break out of loop, EOF reached?
195                                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('FILE-IO-STREAM: EOF reached!');
196                                 break;
197                         }
198
199                         // Remember last read line for avoiding possible infinite loops
200                         $lastBuffer = $inputBuffer;
201                 }
202
203                 // Close directory handle
204                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('FILE-IO-STREAM: Closing file ...');
205                 unset($fileInstance);
206
207                 // Convert it into an array
208                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Read inputBuffer=%d bytes from infoInstance=%s', strlen($inputBuffer), $infoInstance));
209                 $inputArray = explode(chr(10), $inputBuffer);
210
211                 // Now process the read lines and verify it's content
212                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: inputArray()=%d', count($inputArray)));
213                 foreach ($inputArray as $rawLine) {
214                         // Trim it a little but not the leading spaces/tab-stops
215                         $rawLine = rtrim($rawLine);
216
217                         // Analyze this line
218                         /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: rawLine()=%d', strlen($rawLine)));
219                         if (substr($rawLine, 0, 5) == self::FILE_IO_FILE_HEADER_ID) {
220                                 // Header found, so let's extract it
221                                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Found header, rawLine=%s', $rawLine));
222                                 $header = explode(self::FILE_IO_SEPARATOR, $rawLine);
223                                 $headerLine = trim($header[1]);
224
225                                 // Now we must convert it again into an array
226                                 $header = explode(self::FILE_IO_CHUNKER, $headerLine);
227
228                                 // Is the header (maybe) valid?
229                                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: header()=%d', count($header)));
230                                 if (count($header) != 4) {
231                                         // Throw an exception
232                                         throw new InvalidArrayCountException([$this, 'header', count($header), 4], self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
233                                 }
234                         } elseif (substr($rawLine, 0, 5) == self::FILE_IO_DATA_BLOCK_ID) {
235                                 // Is a data line!
236                                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Data line found rawLine=%s', $rawLine));
237                                 $data = explode(self::FILE_IO_SEPARATOR, $rawLine);
238                                 $dataLine = $data[1];
239
240                                 // First element is the data, second the MD5 checksum
241                                 $data = explode(self::FILE_IO_CHUNKER, $dataLine);
242
243                                 // Validate the read line
244                                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: data()=%d', count($data)));
245                                 if (count($data) == 2) {
246                                         // Generate checksum (MD5 is okay here)
247                                         $checksum = md5($data[0]);
248
249                                         // Check if it matches provided one
250                                         /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: checksum=%s,data[1]=%s', $checksum, $data[1]));
251                                         if ($checksum != $data[1]) {
252                                                 // MD5 hash did not match!
253                                                 throw new InvalidMD5ChecksumException([$this, $checksum, $data[1]], self::EXCEPTION_MD5_CHECKSUMS_MISMATCH);
254                                         }
255                                 } else {
256                                         // Invalid count!
257                                         throw new InvalidArrayCountException([$this, 'data', count($data), 2], self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
258                                 }
259
260                                 // Add this to the readData string
261                                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Adding %d raw data to input stream', strlen($data[0])));
262                                 $readData .= $data[0];
263                         } else {
264                                 // Other raw lines than header/data tagged lines and re-add the new-line char
265                                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: Adding rawLine=%s(%d) + PHP_EOL to input stream', $rawLine, strlen($rawLine)));
266                                 $readData .= $rawLine . PHP_EOL;
267                         }
268                 }
269
270                 // Was raw lines read and no header/data?
271                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: readData()=%d,header()=%d,data()=%d', strlen($readData), count($header), count($data)));
272                 if ((!empty($readData)) && (count($header) == 0) && (count($data) == 0)) {
273                         // Return raw lines back
274                         /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: readData()=%d - EXIT!', strlen($readData)));
275                         return $readData;
276                 }
277
278                 // Was a header found?
279                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: header()=%d', count($header)));
280                 if (count($header) != 4) {
281                         // Throw an exception
282                         throw new InvalidArrayCountException([$this, 'header', count($header), 4], self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
283                 }
284
285                 // Decode all from Base64
286                 $decodedData = @base64_decode($readData);
287
288                 // Does the size match?
289                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: decodedData()=%d,header[2]=%d', strlen($decodedData), $header[2]));
290                 if (strlen($decodedData) != $header[2]) {
291                         // Size did not match
292                         throw new InvalidDataLengthException([$this, strlen($decodedData), $header[2]], self::EXCEPTION_UNEXPECTED_STRING_SIZE);
293                 }
294
295                 // Generate checksum from whole read data
296                 $checksum = md5($decodedData);
297
298                 // Validate the decoded data with the final MD5 hash
299                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('FILE-IO-STREAM: checksum=%s,header[3]=%s', $checksum, $header[3]));
300                 if ($checksum != $header[3]) {
301                         // MD5 hash did not match!
302                         throw new InvalidMD5ChecksumException([$this, $checksum, $header[3]], self::EXCEPTION_MD5_CHECKSUMS_MISMATCH);
303                 }
304
305                 // Return all in an array
306                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: header()=%d,decodedData()=%d - EXIT!', count($header), strlen($decodedData)));
307                 return [
308                         'header' => $header,
309                         'data'   => $decodedData,
310                 ];
311         }
312
313         /**
314          * Streams the data and maybe does something to it
315          *
316          * @param       $data   The data (string mostly) to "stream"
317          * @return      $data   The data (string mostly) to "stream"
318          * @throws      UnsupportedOperationException   If this method is called
319          */
320         public function streamData (string $data) {
321                 // Not supported
322                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: data=()=%d - CALLED!', strlen($data)));
323                 throw new UnsupportedOperationException([$this, __FUNCTION__], FrameworkInterface::EXCEPTION_UNSPPORTED_OPERATION);
324         }
325
326         /**
327          * Determines seek position
328          *
329          * @return      $seekPosition   Current seek position
330          * @todo        0% done
331          */
332         public function determineSeekPosition () {
333                 // Trace message
334                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: CALLED!');
335                 DebugMiddleware::getSelfInstance()->partialStub();
336         }
337
338         /**
339          * Seek to given offset (default) or other possibilities as fseek() gives.
340          *
341          * @param       $offset         Offset to seek to (or used as "base" for other seeks)
342          * @param       $whence         Added to offset (default: only use offset to seek to)
343          * @return      $status         Status of file seek: 0 = success, -1 = failed
344          */
345         public function seek (int $offset, int $whence = SEEK_SET) {
346                 // Check parameters
347                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: offset=%d,whence=%d - CALLED!', $offset, $whence));
348                 if ($offset < 0) {
349                         // Throw IAE
350                         throw new InvalidArgumentException(sprintf('offset=%d is below zero', $offset), FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
351                 } elseif ($whence < 0) {
352                         // Throw IAE
353                         throw new InvalidArgumentException(sprintf('whence=%d is below zero', $whence), FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
354                 }
355
356                 DebugMiddleware::getSelfInstance()->partialStub('offset=' . $offset . ',whence=' . $whence);
357         }
358
359         /**
360          * Size of file stack
361          *
362          * @return      $size   Size (in bytes) of file
363          */
364         public function size () {
365                 // Trace message
366                 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: CALLED!');
367                 DebugMiddleware::getSelfInstance()->partialStub();
368         }
369
370 }