3 namespace Org\Mxchange\CoreFramework\Stream\Filesystem;
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;
17 use \InvalidArgumentException;
18 use \OutOfBoundsException;
22 * An universal class for file input/output streams.
24 * @author Roland Haeder <webmaster@shipsimu.org>
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
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.
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.
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/>.
43 class FileIoStream extends BaseFrameworkSystem implements FileInputStreamer, FileOutputStreamer {
45 * File header indicator
47 const FILE_IO_FILE_HEADER_ID = '@head';
50 * Data block indicator
52 const FILE_IO_DATA_BLOCK_ID = '@data';
57 const FILE_IO_CHUNKER = ':';
62 const FILE_IO_SEPARATOR = '^';
65 * Protected constructor
67 private function __construct () {
68 // Call parent constructor
69 parent::__construct(__CLASS__);
73 * Create a file IO stream. This is a class for performing all actions
74 * on files like creating, deleting and loading them.
76 * @return $ioInstance An instance of a FileIoStream class
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();
83 // Return the instance
84 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: ioInstance=%s - EXIT!', $ioInstance->__toString()));
89 * Saves data to a given local file and create missing directory structures
91 * @param $fileInfoInstance An instance of a SplFileInfo class
92 * @param $dataArray The data we shall store to the file
94 * @see FileOutputStreamer
95 * @throws InvalidArgumentException If an invalid parameter was given
96 * @throws OutOfBoundsException If an expected array element wasn't found
98 public final function saveFile (SplFileInfo $fileInfoInstance, array $dataArray) {
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)));
113 // Get a file output pointer
114 $fileInstance = ObjectFactory::createObjectByConfiguredName('file_raw_output_class', [$fileInfoInstance, 'wb']);
115 } catch (FileNotFoundException $e) {
117 ApplicationEntryPoint::exitApplication('The application has made a fatal error. Exception: ' . $e->__toString() . ' with message: ' . $e->getMessage());
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,
126 self::FILE_IO_CHUNKER,
128 self::FILE_IO_CHUNKER,
129 strlen($dataArray[1]),
130 self::FILE_IO_CHUNKER,
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]);
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);
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,
150 self::FILE_IO_CHUNKER,
154 // Advance to the next 50-chars block
159 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('FILE-IO-STREAM: Closing file ...');
160 unset($fileInstance);
163 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: EXIT!');
167 * Reads from a local file
169 * @param $infoInstance An instance of a SplFileInfo class
170 * @return $array An array with the element 'header' and 'data'
171 * @see FileInputStreamer
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));
180 $readData = ''; // This will contain our read data
182 // Get a file input handler
183 $fileInstance = ObjectFactory::createObjectByConfiguredName('file_raw_input_class', array($infoInstance));
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;
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!');
199 // Remember last read line for avoiding possible infinite loops
200 $lastBuffer = $inputBuffer;
203 // Close directory handle
204 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('FILE-IO-STREAM: Closing file ...');
205 unset($fileInstance);
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);
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);
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]);
225 // Now we must convert it again into an array
226 $header = explode(self::FILE_IO_CHUNKER, $headerLine);
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);
234 } elseif (substr($rawLine, 0, 5) == self::FILE_IO_DATA_BLOCK_ID) {
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];
240 // First element is the data, second the MD5 checksum
241 $data = explode(self::FILE_IO_CHUNKER, $dataLine);
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]);
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);
257 throw new InvalidArrayCountException([$this, 'data', count($data), 2], self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
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];
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;
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)));
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);
285 // Decode all from Base64
286 $decodedData = @base64_decode($readData);
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);
295 // Generate checksum from whole read data
296 $checksum = md5($decodedData);
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);
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)));
309 'data' => $decodedData,
314 * Streams the data and maybe does something to it
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
320 public function streamData (string $data) {
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);
327 * Determines seek position
329 * @return $seekPosition Current seek position
332 public function determineSeekPosition () {
334 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: CALLED!');
335 DebugMiddleware::getSelfInstance()->partialStub();
339 * Seek to given offset (default) or other possibilities as fseek() gives.
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
345 public function seek (int $offset, int $whence = SEEK_SET) {
347 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('FILE-IO-STREAM: offset=%d,whence=%d - CALLED!', $offset, $whence));
350 throw new InvalidArgumentException(sprintf('offset=%d is below zero', $offset), FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
351 } elseif ($whence < 0) {
353 throw new InvalidArgumentException(sprintf('whence=%d is below zero', $whence), FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
356 DebugMiddleware::getSelfInstance()->partialStub('offset=' . $offset . ',whence=' . $whence);
362 * @return $size Size (in bytes) of file
364 public function size () {
366 /* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('FILE-IO-STREAM: CALLED!');
367 DebugMiddleware::getSelfInstance()->partialStub();