1 /* SPDX-License-Identifier: BSL-1.0 OR BSD-3-Clause */
2
3 #ifndef MPT_IO_READ_FILEREADER_HPP
4 #define MPT_IO_READ_FILEREADER_HPP
5
6
7
8 #include "mpt/base/alloc.hpp"
9 #include "mpt/base/bit.hpp"
10 #include "mpt/base/integer.hpp"
11 #include "mpt/base/memory.hpp"
12 #include "mpt/base/namespace.hpp"
13 #include "mpt/base/span.hpp"
14 #include "mpt/base/utility.hpp"
15 #include "mpt/io/base.hpp"
16 #include "mpt/endian/floatingpoint.hpp"
17 #include "mpt/endian/integer.hpp"
18 #include "mpt/string/utility.hpp"
19
20 #include <algorithm>
21 #include <array>
22 #include <limits>
23 #include <string>
24 #include <vector>
25
26 #include <cassert>
27 #include <cstddef>
28 #include <cstring>
29
30
31
32 namespace mpt {
33 inline namespace MPT_INLINE_NS {
34
35
36
37 namespace IO {
38
39
40
41 namespace FileReader {
42
43 // Read a "T" object from the stream.
44 // If not enough bytes can be read, false is returned.
45 // If successful, the file cursor is advanced by the size of "T".
46 template <typename T, typename TFileCursor>
Read(TFileCursor & f,T & target)47 bool Read(TFileCursor & f, T & target) {
48 // cppcheck false-positive
49 // cppcheck-suppress uninitvar
50 mpt::byte_span dst = mpt::as_raw_memory(target);
51 if (dst.size() != f.GetRaw(dst).size()) {
52 return false;
53 }
54 f.Skip(dst.size());
55 return true;
56 }
57
58 // Read an array of binary-safe T values.
59 // If successful, the file cursor is advanced by the size of the array.
60 // Otherwise, the target is zeroed.
61 template <typename T, std::size_t destSize, typename TFileCursor>
ReadArray(TFileCursor & f,T (& destArray)[destSize])62 bool ReadArray(TFileCursor & f, T (&destArray)[destSize]) {
63 static_assert(mpt::is_binary_safe<T>::value);
64 if (!f.CanRead(sizeof(destArray))) {
65 mpt::reset(destArray);
66 return false;
67 }
68 f.ReadRaw(mpt::as_raw_memory(destArray));
69 return true;
70 }
71
72 // Read an array of binary-safe T values.
73 // If successful, the file cursor is advanced by the size of the array.
74 // Otherwise, the target is zeroed.
75 template <typename T, std::size_t destSize, typename TFileCursor>
ReadArray(TFileCursor & f,std::array<T,destSize> & destArray)76 bool ReadArray(TFileCursor & f, std::array<T, destSize> & destArray) {
77 static_assert(mpt::is_binary_safe<T>::value);
78 if (!f.CanRead(sizeof(destArray))) {
79 destArray.fill(T{});
80 return false;
81 }
82 f.ReadRaw(mpt::as_raw_memory(destArray));
83 return true;
84 }
85
86 // Read destSize elements of binary-safe type T into a vector.
87 // If successful, the file cursor is advanced by the size of the vector.
88 // Otherwise, the vector is resized to destSize, but possibly existing contents are not cleared.
89 template <typename T, typename TFileCursor>
ReadVector(TFileCursor & f,std::vector<T> & destVector,size_t destSize)90 bool ReadVector(TFileCursor & f, std::vector<T> & destVector, size_t destSize) {
91 static_assert(mpt::is_binary_safe<T>::value);
92 destVector.resize(destSize);
93 if (!f.CanRead(sizeof(T) * destSize)) {
94 return false;
95 }
96 f.ReadRaw(mpt::as_raw_memory(destVector));
97 return true;
98 }
99
100 template <typename T, std::size_t destSize, typename TFileCursor>
ReadArray(TFileCursor & f)101 std::array<T, destSize> ReadArray(TFileCursor & f) {
102 std::array<T, destSize> destArray;
103 ReadArray(f, destArray);
104 return destArray;
105 }
106
107 // Read some kind of integer in little-endian format.
108 // If successful, the file cursor is advanced by the size of the integer.
109 template <typename T, typename TFileCursor>
110 T ReadIntLE(TFileCursor & f) {
111 static_assert(std::numeric_limits<T>::is_integer == true, "Target type is a not an integer");
112 typename mpt::make_le<T>::type target;
113 if (!FileReader::Read(f, target)) {
114 return 0;
115 }
116 return target;
117 }
118
119 // Read some kind of integer in big-endian format.
120 // If successful, the file cursor is advanced by the size of the integer.
121 template <typename T, typename TFileCursor>
122 T ReadIntBE(TFileCursor & f) {
123 static_assert(std::numeric_limits<T>::is_integer == true, "Target type is a not an integer");
124 typename mpt::make_be<T>::type target;
125 if (!FileReader::Read(f, target)) {
126 return 0;
127 }
128 return target;
129 }
130
131 // Read a integer in little-endian format which has some of its higher bytes not stored in file.
132 // If successful, the file cursor is advanced by the given size.
133 template <typename T, typename TFileCursor>
134 T ReadTruncatedIntLE(TFileCursor & f, typename TFileCursor::pos_type size) {
135 static_assert(std::numeric_limits<T>::is_integer == true, "Target type is a not an integer");
136 assert(sizeof(T) >= size);
137 if (size == 0) {
138 return 0;
139 }
140 if (!f.CanRead(size)) {
141 return 0;
142 }
143 uint8 buf[sizeof(T)];
144 bool negative = false;
145 for (std::size_t i = 0; i < sizeof(T); ++i) {
146 uint8 byte = 0;
147 if (i < size) {
148 FileReader::Read(f, byte);
149 negative = std::numeric_limits<T>::is_signed && ((byte & 0x80) != 0x00);
150 } else {
151 // sign or zero extend
152 byte = negative ? 0xff : 0x00;
153 }
154 buf[i] = byte;
155 }
156 return mpt::bit_cast<typename mpt::make_le<T>::type>(buf);
157 }
158
159 // Read a supplied-size little endian integer to a fixed size variable.
160 // The data is properly sign-extended when fewer bytes are stored.
161 // If more bytes are stored, higher order bytes are silently ignored.
162 // If successful, the file cursor is advanced by the given size.
163 template <typename T, typename TFileCursor>
164 T ReadSizedIntLE(TFileCursor & f, typename TFileCursor::pos_type size) {
165 static_assert(std::numeric_limits<T>::is_integer == true, "Target type is a not an integer");
166 if (size == 0) {
167 return 0;
168 }
169 if (!f.CanRead(size)) {
170 return 0;
171 }
172 if (size < sizeof(T)) {
173 return FileReader::ReadTruncatedIntLE<T>(f, size);
174 }
175 T retval = FileReader::ReadIntLE<T>(f);
176 f.Skip(size - sizeof(T));
177 return retval;
178 }
179
180 // Read unsigned 32-Bit integer in little-endian format.
181 // If successful, the file cursor is advanced by the size of the integer.
182 template <typename TFileCursor>
ReadUint32LE(TFileCursor & f)183 uint32 ReadUint32LE(TFileCursor & f) {
184 return FileReader::ReadIntLE<uint32>(f);
185 }
186
187 // Read unsigned 32-Bit integer in big-endian format.
188 // If successful, the file cursor is advanced by the size of the integer.
189 template <typename TFileCursor>
ReadUint32BE(TFileCursor & f)190 uint32 ReadUint32BE(TFileCursor & f) {
191 return FileReader::ReadIntBE<uint32>(f);
192 }
193
194 // Read signed 32-Bit integer in little-endian format.
195 // If successful, the file cursor is advanced by the size of the integer.
196 template <typename TFileCursor>
ReadInt32LE(TFileCursor & f)197 int32 ReadInt32LE(TFileCursor & f) {
198 return FileReader::ReadIntLE<int32>(f);
199 }
200
201 // Read signed 32-Bit integer in big-endian format.
202 // If successful, the file cursor is advanced by the size of the integer.
203 template <typename TFileCursor>
ReadInt32BE(TFileCursor & f)204 int32 ReadInt32BE(TFileCursor & f) {
205 return FileReader::ReadIntBE<int32>(f);
206 }
207
208 // Read unsigned 24-Bit integer in little-endian format.
209 // If successful, the file cursor is advanced by the size of the integer.
210 template <typename TFileCursor>
ReadUint24LE(TFileCursor & f)211 uint32 ReadUint24LE(TFileCursor & f) {
212 const auto arr = FileReader::ReadArray<uint8, 3>(f);
213 return arr[0] | (arr[1] << 8) | (arr[2] << 16);
214 }
215
216 // Read unsigned 24-Bit integer in big-endian format.
217 // If successful, the file cursor is advanced by the size of the integer.
218 template <typename TFileCursor>
ReadUint24BE(TFileCursor & f)219 uint32 ReadUint24BE(TFileCursor & f) {
220 const auto arr = FileReader::ReadArray<uint8, 3>(f);
221 return (arr[0] << 16) | (arr[1] << 8) | arr[2];
222 }
223
224 // Read unsigned 16-Bit integer in little-endian format.
225 // If successful, the file cursor is advanced by the size of the integer.
226 template <typename TFileCursor>
ReadUint16LE(TFileCursor & f)227 uint16 ReadUint16LE(TFileCursor & f) {
228 return FileReader::ReadIntLE<uint16>(f);
229 }
230
231 // Read unsigned 16-Bit integer in big-endian format.
232 // If successful, the file cursor is advanced by the size of the integer.
233 template <typename TFileCursor>
ReadUint16BE(TFileCursor & f)234 uint16 ReadUint16BE(TFileCursor & f) {
235 return FileReader::ReadIntBE<uint16>(f);
236 }
237
238 // Read signed 16-Bit integer in little-endian format.
239 // If successful, the file cursor is advanced by the size of the integer.
240 template <typename TFileCursor>
ReadInt16LE(TFileCursor & f)241 int16 ReadInt16LE(TFileCursor & f) {
242 return FileReader::ReadIntLE<int16>(f);
243 }
244
245 // Read signed 16-Bit integer in big-endian format.
246 // If successful, the file cursor is advanced by the size of the integer.
247 template <typename TFileCursor>
ReadInt16BE(TFileCursor & f)248 int16 ReadInt16BE(TFileCursor & f) {
249 return FileReader::ReadIntBE<int16>(f);
250 }
251
252 // Read a single 8bit character.
253 // If successful, the file cursor is advanced by the size of the integer.
254 template <typename TFileCursor>
ReadChar(TFileCursor & f)255 char ReadChar(TFileCursor & f) {
256 char target;
257 if (!FileReader::Read(f, target)) {
258 return 0;
259 }
260 return target;
261 }
262
263 // Read unsigned 8-Bit integer.
264 // If successful, the file cursor is advanced by the size of the integer.
265 template <typename TFileCursor>
ReadUint8(TFileCursor & f)266 uint8 ReadUint8(TFileCursor & f) {
267 uint8 target;
268 if (!FileReader::Read(f, target)) {
269 return 0;
270 }
271 return target;
272 }
273
274 // Read signed 8-Bit integer. If successful, the file cursor is advanced by the size of the integer.
275 template <typename TFileCursor>
ReadInt8(TFileCursor & f)276 int8 ReadInt8(TFileCursor & f) {
277 int8 target;
278 if (!FileReader::Read(f, target)) {
279 return 0;
280 }
281 return target;
282 }
283
284 // Read 32-Bit float in little-endian format.
285 // If successful, the file cursor is advanced by the size of the float.
286 template <typename TFileCursor>
ReadFloatLE(TFileCursor & f)287 float ReadFloatLE(TFileCursor & f) {
288 IEEE754binary32LE target;
289 if (!FileReader::Read(f, target)) {
290 return 0.0f;
291 }
292 return target;
293 }
294
295 // Read 32-Bit float in big-endian format.
296 // If successful, the file cursor is advanced by the size of the float.
297 template <typename TFileCursor>
ReadFloatBE(TFileCursor & f)298 float ReadFloatBE(TFileCursor & f) {
299 IEEE754binary32BE target;
300 if (!FileReader::Read(f, target)) {
301 return 0.0f;
302 }
303 return target;
304 }
305
306 // Read 64-Bit float in little-endian format.
307 // If successful, the file cursor is advanced by the size of the float.
308 template <typename TFileCursor>
ReadDoubleLE(TFileCursor & f)309 double ReadDoubleLE(TFileCursor & f) {
310 IEEE754binary64LE target;
311 if (!FileReader::Read(f, target)) {
312 return 0.0;
313 }
314 return target;
315 }
316
317 // Read 64-Bit float in big-endian format.
318 // If successful, the file cursor is advanced by the size of the float.
319 template <typename TFileCursor>
ReadDoubleBE(TFileCursor & f)320 double ReadDoubleBE(TFileCursor & f) {
321 IEEE754binary64BE target;
322 if (!FileReader::Read(f, target)) {
323 return 0.0;
324 }
325 return target;
326 }
327
328 // Read a struct.
329 // If successful, the file cursor is advanced by the size of the struct. Otherwise, the target is zeroed.
330 template <typename T, typename TFileCursor>
ReadStruct(TFileCursor & f,T & target)331 bool ReadStruct(TFileCursor & f, T & target) {
332 static_assert(mpt::is_binary_safe<T>::value);
333 if (!FileReader::Read(f, target)) {
334 mpt::reset(target);
335 return false;
336 }
337 return true;
338 }
339
340 // Allow to read a struct partially (if there's less memory available than the struct's size, fill it up with zeros).
341 // The file cursor is advanced by "partialSize" bytes.
342 template <typename T, typename TFileCursor>
ReadStructPartial(TFileCursor & f,T & target,typename TFileCursor::pos_type partialSize=sizeof (T))343 typename TFileCursor::pos_type ReadStructPartial(TFileCursor & f, T & target, typename TFileCursor::pos_type partialSize = sizeof(T)) {
344 static_assert(mpt::is_binary_safe<T>::value);
345 typename TFileCursor::pos_type copyBytes = std::min(partialSize, sizeof(T));
346 if (!f.CanRead(copyBytes)) {
347 copyBytes = f.BytesLeft();
348 }
349 f.GetRaw(mpt::span(mpt::as_raw_memory(target).data(), copyBytes));
350 std::memset(mpt::as_raw_memory(target).data() + copyBytes, 0, sizeof(target) - copyBytes);
351 f.Skip(partialSize);
352 return copyBytes;
353 }
354
355 // Read a null-terminated string into a std::string
356 template <typename TFileCursor>
ReadNullString(TFileCursor & f,std::string & dest,const typename TFileCursor::pos_type maxLength=std::numeric_limits<typename TFileCursor::pos_type>::max ())357 bool ReadNullString(TFileCursor & f, std::string & dest, const typename TFileCursor::pos_type maxLength = std::numeric_limits<typename TFileCursor::pos_type>::max()) {
358 dest.clear();
359 if (!f.CanRead(1)) {
360 return false;
361 }
362 char buffer[mpt::IO::BUFFERSIZE_MINUSCULE];
363 typename TFileCursor::pos_type avail = 0;
364 while ((avail = std::min(f.GetRaw(mpt::as_span(buffer)).size(), maxLength - dest.length())) != 0) {
365 auto end = std::find(buffer, buffer + avail, '\0');
366 dest.insert(dest.end(), buffer, end);
367 f.Skip(end - buffer);
368 if (end < buffer + avail) {
369 // Found null char
370 f.Skip(1);
371 break;
372 }
373 }
374 return dest.length() != 0;
375 }
376
377 // Read a string up to the next line terminator into a std::string
378 template <typename TFileCursor>
ReadLine(TFileCursor & f,std::string & dest,const typename TFileCursor::pos_type maxLength=std::numeric_limits<typename TFileCursor::pos_type>::max ())379 bool ReadLine(TFileCursor & f, std::string & dest, const typename TFileCursor::pos_type maxLength = std::numeric_limits<typename TFileCursor::pos_type>::max()) {
380 dest.clear();
381 if (!f.CanRead(1)) {
382 return false;
383 }
384 char buffer[mpt::IO::BUFFERSIZE_MINUSCULE];
385 char c = '\0';
386 typename TFileCursor::pos_type avail = 0;
387 while ((avail = std::min(f.GetRaw(mpt::as_span(buffer)).size(), maxLength - dest.length())) != 0) {
388 auto end = std::find_if(buffer, buffer + avail, mpt::is_any_line_ending<char>);
389 dest.insert(dest.end(), buffer, end);
390 f.Skip(end - buffer);
391 if (end < buffer + avail) {
392 // Found line ending
393 f.Skip(1);
394 // Handle CRLF line ending
395 if (*end == '\r') {
396 if (FileReader::Read(f, c) && c != '\n') {
397 f.SkipBack(1);
398 }
399 }
400 break;
401 }
402 }
403 return true;
404 }
405
406 // Compare a magic string with the current stream position.
407 // Returns true if they are identical and advances the file cursor by the the length of the "magic" string.
408 // Returns false if the string could not be found. The file cursor is not advanced in this case.
409 template <size_t N, typename TFileCursor>
ReadMagic(TFileCursor & f,const char (& magic)[N])410 bool ReadMagic(TFileCursor & f, const char (&magic)[N]) {
411 assert(magic[N - 1] == '\0');
412 for (std::size_t i = 0; i < N - 1; ++i) {
413 assert(magic[i] != '\0');
414 }
415 constexpr typename TFileCursor::pos_type magicLength = N - 1;
416 std::byte buffer[magicLength] = {};
417 if (f.GetRaw(mpt::span(buffer, magicLength)).size() != magicLength) {
418 return false;
419 }
420 if (std::memcmp(buffer, magic, magicLength)) {
421 return false;
422 }
423 f.Skip(magicLength);
424 return true;
425 }
426
427 // Read variable-length unsigned integer (as found in MIDI files).
428 // If successful, the file cursor is advanced by the size of the integer and true is returned.
429 // False is returned if not enough bytes were left to finish reading of the integer or if an overflow happened (source doesn't fit into target integer).
430 // In case of an overflow, the target is also set to the maximum value supported by its data type.
431 template <typename T, typename TFileCursor>
ReadVarInt(TFileCursor & f,T & target)432 bool ReadVarInt(TFileCursor & f, T & target) {
433 static_assert(std::numeric_limits<T>::is_integer == true && std::numeric_limits<T>::is_signed == false, "Target type is not an unsigned integer");
434
435 if (f.NoBytesLeft()) {
436 target = 0;
437 return false;
438 }
439
440 std::byte bytes[16]; // More than enough for any valid VarInt
441 typename TFileCursor::pos_type avail = f.GetRaw(mpt::as_span(bytes)).size();
442 typename TFileCursor::pos_type readPos = 1;
443
444 uint8 b = mpt::byte_cast<uint8>(bytes[0]);
445 target = (b & 0x7F);
446 std::size_t writtenBits = static_cast<std::size_t>(mpt::bit_width(target)); // Bits used in the most significant byte
447
448 while (readPos < avail && (b & 0x80) != 0) {
449 b = mpt::byte_cast<uint8>(bytes[readPos++]);
450 target <<= 7;
451 target |= (b & 0x7F);
452 writtenBits += 7;
453 if (readPos == avail) {
454 f.Skip(readPos);
455 avail = f.GetRaw(mpt::as_span(bytes)).size();
456 readPos = 0;
457 }
458 }
459 f.Skip(readPos);
460
461 if (writtenBits > sizeof(target) * 8u) {
462 // Overflow
463 target = std::numeric_limits<T>::max();
464 return false;
465 } else if ((b & 0x80) != 0) {
466 // Reached EOF
467 return false;
468 }
469 return true;
470 }
471
472 template <typename Tid, typename Tsize>
473 struct ChunkHeader {
474 using id_type = Tid;
475 using size_type = Tsize;
declare_binary_safe(const ChunkHeader &)476 friend constexpr bool declare_binary_safe(const ChunkHeader &) noexcept {
477 return true;
478 }
479 id_type id{};
480 size_type size{};
GetIDmpt::MPT_INLINE_NS::IO::FileReader::ChunkHeader481 id_type GetID() const {
482 return id;
483 }
GetLengthmpt::MPT_INLINE_NS::IO::FileReader::ChunkHeader484 size_type GetLength() const {
485 return size;
486 }
487 };
488
489 template <typename TChunkHeader, typename TFileCursor>
490 struct Chunk {
491 TChunkHeader header;
492 TFileCursor data;
GetHeadermpt::MPT_INLINE_NS::IO::FileReader::Chunk493 TChunkHeader GetHeader() const {
494 return header;
495 }
GetDatampt::MPT_INLINE_NS::IO::FileReader::Chunk496 TFileCursor GetData() const {
497 return data;
498 }
499 };
500
501 template <typename TChunkHeader, typename TFileCursor>
502 struct ChunkList {
503
504 using id_type = decltype(TChunkHeader().GetID());
505 using size_type = decltype(TChunkHeader().GetLength());
506
507 std::vector<Chunk<TChunkHeader, TFileCursor>> chunks;
508
509 // Check if the list contains a given chunk.
ChunkExistsmpt::MPT_INLINE_NS::IO::FileReader::ChunkList510 bool ChunkExists(id_type id) const {
511 return std::find_if(chunks.begin(), chunks.end(), [id](const Chunk<TChunkHeader, TFileCursor> & chunk) { return chunk.GetHeader().GetID() == id; }) != chunks.end();
512 }
513
514 // Retrieve the first chunk with a given ID.
GetChunkmpt::MPT_INLINE_NS::IO::FileReader::ChunkList515 TFileCursor GetChunk(id_type id) const {
516 auto chunk = std::find_if(chunks.begin(), chunks.end(), [id](const Chunk<TChunkHeader, TFileCursor> & chunk) { return chunk.GetHeader().GetID() == id; });
517 if (chunk == chunks.end()) {
518 return TFileCursor();
519 }
520 return chunk->GetData();
521 }
522
523 // Retrieve all chunks with a given ID.
GetAllChunksmpt::MPT_INLINE_NS::IO::FileReader::ChunkList524 std::vector<TFileCursor> GetAllChunks(id_type id) const {
525 std::vector<TFileCursor> result;
526 for (const auto & chunk : chunks) {
527 if (chunk.GetHeader().GetID() == id) {
528 result.push_back(chunk.GetData());
529 }
530 }
531 return result;
532 }
533 };
534
535 // Read a single "TChunkHeader" chunk.
536 // T is required to have the methods GetID() and GetLength().
537 // GetLength() must return the chunk size in bytes, and GetID() the chunk ID.
538 template <typename TChunkHeader, typename TFileCursor>
ReadNextChunk(TFileCursor & f,typename TFileCursor::pos_type alignment)539 Chunk<TChunkHeader, TFileCursor> ReadNextChunk(TFileCursor & f, typename TFileCursor::pos_type alignment) {
540 Chunk<TChunkHeader, TFileCursor> result;
541 if (!FileReader::Read(f, result.header)) {
542 return Chunk<TChunkHeader, TFileCursor>();
543 }
544 typename TFileCursor::pos_type dataSize = result.header.GetLength();
545 result.data = f.ReadChunk(dataSize);
546 if (alignment > 1) {
547 if ((dataSize % alignment) != 0) {
548 f.Skip(alignment - (dataSize % alignment));
549 }
550 }
551 return result;
552 }
553
554 // Read a series of "TChunkHeader" chunks until the end of file is reached.
555 // T is required to have the methods GetID() and GetLength().
556 // GetLength() must return the chunk size in bytes, and GetID() the chunk ID.
557 template <typename TChunkHeader, typename TFileCursor>
ReadChunks(TFileCursor & f,typename TFileCursor::pos_type alignment)558 ChunkList<TChunkHeader, TFileCursor> ReadChunks(TFileCursor & f, typename TFileCursor::pos_type alignment) {
559 ChunkList<TChunkHeader, TFileCursor> result;
560 while (f.CanRead(sizeof(TChunkHeader))) {
561 result.chunks.push_back(FileReader::ReadNextChunk<TChunkHeader, TFileCursor>(f, alignment));
562 }
563 return result;
564 }
565
566 // Read a series of "TChunkHeader" chunks until a given chunk ID is found.
567 // T is required to have the methods GetID() and GetLength().
568 // GetLength() must return the chunk size in bytes, and GetID() the chunk ID.
569 template <typename TChunkHeader, typename TFileCursor>
570 ChunkList<TChunkHeader, TFileCursor> ReadChunksUntil(TFileCursor & f, typename TFileCursor::pos_type alignment, decltype(TChunkHeader().GetID()) lastID) {
571 ChunkList<TChunkHeader, TFileCursor> result;
572 while (f.CanRead(sizeof(TChunkHeader))) {
573 result.chunks.push_back(FileReader::ReadNextChunk<TChunkHeader, TFileCursor>(f, alignment));
574 if (result.chunks.back().GetHeader().GetID() == lastID) {
575 break;
576 }
577 }
578 return result;
579 }
580
581 } // namespace FileReader
582
583
584
585 } // namespace IO
586
587
588
589 } // namespace MPT_INLINE_NS
590 } // namespace mpt
591
592
593
594 #endif // MPT_IO_READ_FILEREADER_HPP
595