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