#pragma once #if !defined(MIJIN_IO_STREAM_HPP_INCLUDED) #define MIJIN_IO_STREAM_HPP_INCLUDED 1 #include #include #include #include #include #include #include "../async/coroutine.hpp" #include "../container/typeless_buffer.hpp" #include "../internal/common.hpp" #include "../types/result.hpp" #include "../util/exception.hpp" namespace mijin { // // public defines // // // public constants // // // public types // enum class SeekMode { ABSOLUTE, RELATIVE, RELATIVE_TO_END }; struct ReadOptions { bool partial : 1 = false; bool peek : 1 = false; bool noBlock : 1 = false; }; struct StreamFeatures { bool read : 1 = false; bool write : 1 = false; bool tell : 1 = false; bool seek : 1 = false; bool async : 1 = false; ReadOptions readOptions = {}; }; enum class FileOpenMode { READ, WRITE, APPEND, READ_WRITE }; enum class [[nodiscard]] StreamError { SUCCESS = 0, IO_ERROR = 1, NOT_SUPPORTED = 2, CONNECTION_CLOSED = 3, PROTOCOL_ERROR = 4, WOULD_BLOCK = 5, CONNECTION_REFUSED = 6, UNKNOWN_ERROR = -1 }; class Stream { public: virtual ~Stream() = default; public: virtual StreamError readRaw(std::span buffer, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) = 0; [[deprecated("Partial parameter has been replaced with options.")]] StreamError readRaw(std::span buffer, bool partial, std::size_t* outBytesRead = nullptr) { return readRaw(buffer, {.partial = partial}, outBytesRead); } virtual StreamError writeRaw(std::span buffer) = 0; virtual std::size_t tell() = 0; virtual StreamError seek(std::intptr_t pos, SeekMode seekMode = SeekMode::ABSOLUTE) = 0; virtual void flush(); virtual bool isAtEnd() = 0; virtual StreamFeatures getFeatures() = 0; // async interface (requires getFeatures().async to be set) virtual mijin::Task c_readRaw(std::span buffer, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr); virtual mijin::Task c_writeRaw(std::span buffer); StreamError readRaw(void* outData, std::size_t bytes, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) { std::uint8_t* ptr = static_cast(outData); return readRaw(std::span(ptr, ptr + bytes), options, outBytesRead); } [[deprecated("Partial parameter has been replaced with options.")]] StreamError readRaw(void* outData, std::size_t bytes, bool partial, std::size_t* outBytesRead = nullptr) { std::uint8_t* ptr = static_cast(outData); return readRaw(std::span(ptr, ptr + bytes), {.partial = partial}, outBytesRead); } mijin::Task c_readRaw(void* outData, std::size_t bytes, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) { std::uint8_t* ptr = static_cast(outData); co_return co_await c_readRaw(std::span(ptr, ptr + bytes), options, outBytesRead); } template StreamError readRaw(TRange& range, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) { const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t); return readRaw(&*range.begin(), bytes, options, outBytesRead); } template [[deprecated("Partial parameter has been replaced with options.")]] StreamError readRaw(TRange& range, bool partial, std::size_t* outBytesRead = nullptr) { const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t); return readRaw(&*range.begin(), bytes, {.partial = partial}, outBytesRead); } template typename TAllocator> StreamError readRaw(BaseTypelessBuffer& buffer, const ReadOptions& options = {}) { return readRaw(buffer.data(), buffer.byteSize(), options); } template mijin::Task c_readRaw(TRange& range, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) { const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t); return c_readRaw(&*range.begin(), bytes, options, outBytesRead); } template typename TAllocator> mijin::Task c_readRaw(BaseTypelessBuffer& buffer, const ReadOptions& options = {}) { return c_readRaw(buffer.data(), buffer.byteSize(), options); } StreamError writeRaw(const void* data, std::size_t bytes) { const std::uint8_t* ptr = static_cast(data); return writeRaw(std::span(ptr, ptr + bytes)); } mijin::Task c_writeRaw(const void* data, std::size_t bytes) { const std::uint8_t* ptr = static_cast(data); return c_writeRaw(std::span(ptr, ptr + bytes)); } template StreamError writeRaw(const TRange& range) { const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t); return writeRaw(&*range.begin(), bytes); } template mijin::Task c_writeRaw(const TRange& range) { const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t); return c_writeRaw(&*range.begin(), bytes); } template StreamError read(T& value, const ReadOptions& options = {}) { MIJIN_ASSERT(!options.partial, "Cannot partially read a value."); return readRaw(&value, sizeof(T), options); } template mijin::Task c_read(T& value, const ReadOptions& options = {}) { MIJIN_ASSERT(!options.partial, "Cannot partially read a value."); return c_readRaw(&value, sizeof(T), options); } template StreamError readSpan(T& values, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) { auto asSpan = std::span(values); return readRaw(asSpan.data(), asSpan.size_bytes(), options, outBytesRead); } template mijin::Task c_readSpan(T& values, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) { auto asSpan = std::span(values); return c_readRaw(asSpan.data(), asSpan.size_bytes(), options, outBytesRead); } template StreamError readSpan(TItBegin&& begin, TItEnd&& end, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) { auto asSpan = std::span(std::forward(begin), std::forward(end)); return readRaw(asSpan.data(), asSpan.size_bytes(), options, outBytesRead); } template mijin::Task c_readSpan(TItBegin&& begin, TItEnd&& end, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) { auto asSpan = std::span(std::forward(begin), std::forward(end)); return c_readRaw(asSpan.data(), asSpan.size_bytes(), options, outBytesRead); } template StreamError write(const T& value) { return writeRaw(&value, sizeof(T)); } template mijin::Task c_write(const T& value) requires(std::is_trivial_v) { return c_writeRaw(&value, sizeof(T)); } template StreamError writeSpan(const T& values) { auto asSpan = std::span(values); return writeRaw(asSpan.data(), asSpan.size_bytes()); } template mijin::Task c_writeSpan(const T& values) { auto asSpan = std::span(values); return c_writeRaw(asSpan.data(), asSpan.size_bytes()); } template StreamError writeSpan(TItBegin&& begin, TItEnd&& end) { return writeSpan(std::span(std::forward(begin), std::forward(end))); } template mijin::Task c_writeSpan(TItBegin&& begin, TItEnd&& end) { return c_writeSpan(std::span(std::forward(begin), std::forward(end))); } StreamError readBinaryString(std::string& outString); StreamError writeBinaryString(std::string_view str); StreamError readZString(std::string& outString); StreamError writeZString(std::string_view str); mijin::Task c_readBinaryString(std::string& outString); mijin::Task c_writeBinaryString(std::string_view str); [[deprecated("Use readBinaryString() or readAsString() instead.")]] inline StreamError readString(std::string& outString) { return readBinaryString(outString); } [[deprecated("Use writeBinaryString() or writeText() instead.")]] inline StreamError writeString(std::string_view str) { return writeBinaryString(str); } StreamError getTotalLength(std::size_t& outLength); StreamError copyTo(Stream& otherStream); template typename TAllocator> StreamError readRest(BaseTypelessBuffer& outBuffer); template typename TAllocator> mijin::Task c_readRest(BaseTypelessBuffer& outBuffer); StreamError readLine(std::string& outString); mijin::Task c_readLine(std::string& outString); template, typename TAllocator = MIJIN_DEFAULT_ALLOCATOR> StreamError readAsString(std::basic_string& outString); template, typename TAllocator = MIJIN_DEFAULT_ALLOCATOR> mijin::Task c_readAsString(std::basic_string& outString); StreamError writeText(std::string_view str) { return writeSpan(str); } mijin::Task c_writeText(std::string_view str) { return c_writeSpan(str); } }; class FileStream : public Stream { private: std::FILE* handle = nullptr; // TODO: wrap in gsl::owner<> FileOpenMode mode; std::size_t length = 0; public: ~FileStream() override; StreamError open(const char* path, FileOpenMode mode_); inline StreamError open(const std::string& path, FileOpenMode mode_) { return open(path.c_str(), mode_); } void close(); [[nodiscard]] inline bool isOpen() const { return handle != nullptr; } // Stream overrides StreamError readRaw(std::span buffer, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) override; StreamError writeRaw(std::span buffer) override; std::size_t tell() override; StreamError seek(std::intptr_t pos, SeekMode seekMode = SeekMode::ABSOLUTE) override; void flush() override; bool isAtEnd() override; StreamFeatures getFeatures() override; }; class MemoryStream : public Stream { private: std::span data_; std::size_t pos_ = 0; bool canWrite_ = false; public: void openRW(std::span data); template void openRO(std::span data) { openROImpl(data.data(), data.size_bytes()); } template inline void openRO(std::basic_string_view stringView) { openROImpl(stringView.data(), stringView.size() * sizeof(TChar)); } void close(); [[nodiscard]] inline bool isOpen() const { return data_.data() != nullptr; } [[nodiscard]] inline std::size_t availableBytes() const { MIJIN_ASSERT(isOpen(), "MemoryStream is not open."); return data_.size() - pos_; } // Stream overrides StreamError readRaw(std::span buffer, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) override; StreamError writeRaw(std::span buffer) override; std::size_t tell() override; StreamError seek(std::intptr_t pos, SeekMode seekMode = SeekMode::ABSOLUTE) override; bool isAtEnd() override; StreamFeatures getFeatures() override; private: void openROImpl(const void* data, std::size_t bytes); }; template using StreamResult = ResultBase; // // public functions // template typename TAllocator> StreamError Stream::readRest(BaseTypelessBuffer& outBuffer) { // first try to allocate everything at once std::size_t length = 0; if (const StreamError lengthError = getTotalLength(length); lengthError == StreamError::SUCCESS) { MIJIN_ASSERT(getFeatures().tell, "How did you find the length if you cannot tell()?"); length -= tell(); outBuffer.resize(length); if (const StreamError error = readRaw(outBuffer.data(), length); error != StreamError::SUCCESS) { return error; } return StreamError::SUCCESS; } // could not determine the size, read chunk-wise static constexpr std::size_t CHUNK_SIZE = 4096; std::array chunk = {}; while (!isAtEnd()) { std::size_t bytesRead = 0; if (const StreamError error = readRaw(chunk, {.partial = true}, &bytesRead); error != StreamError::SUCCESS) { return error; } outBuffer.resize(outBuffer.byteSize() + bytesRead); const std::span bufferBytes = outBuffer.template makeSpan(); std::copy_n(chunk.begin(), bytesRead, bufferBytes.end() - static_cast(bytesRead)); } return StreamError::SUCCESS; } template typename TAllocator> mijin::Task Stream::c_readRest(BaseTypelessBuffer& outBuffer) { // first try to allocate everything at once std::size_t length = 0; if (StreamError lengthError = getTotalLength(length); lengthError == StreamError::SUCCESS) { MIJIN_ASSERT(getFeatures().tell, "How did you find the length if you cannot tell()?"); length -= tell(); outBuffer.resize(length); if (StreamError error = co_await c_readRaw(outBuffer.data(), length); error != StreamError::SUCCESS) { co_return error; } co_return StreamError::SUCCESS; } // could not determine the size, read chunk-wise static constexpr std::size_t CHUNK_SIZE = 4096; std::array chunk = {}; while (!isAtEnd()) { std::size_t bytesRead = 0; if (StreamError error = co_await c_readRaw(chunk, {.partial = true}, &bytesRead); error != StreamError::SUCCESS) { co_return error; } outBuffer.resize(outBuffer.byteSize() + bytesRead); std::span bufferBytes = outBuffer.template makeSpan(); std::copy_n(chunk.begin(), bytesRead, bufferBytes.end() - static_cast(bytesRead)); } co_return StreamError::SUCCESS; } template StreamError Stream::readAsString(std::basic_string& outString) { static_assert(sizeof(TChar) == 1, "Can only read to 8-bit character types (char, unsigned char or char8_t"); // first try to allocate everything at once std::size_t length = 0; if (StreamError lengthError = getTotalLength(length); lengthError == StreamError::SUCCESS) { MIJIN_ASSERT(getFeatures().tell, "How did you find the length if you cannot tell()?"); length -= tell(); outString.resize(length); if (StreamError error = readRaw(outString.data(), length); error != StreamError::SUCCESS) { return error; } return StreamError::SUCCESS; } // could not determine the size, read chunk-wise static constexpr std::size_t CHUNK_SIZE = 4096; std::array chunk; outString.clear(); while (!isAtEnd()) { std::size_t bytesRead = 0; if (StreamError error = readRaw(chunk, {.partial = true}, &bytesRead); error != StreamError::SUCCESS) { return error; } outString.append(chunk.data(), chunk.data() + bytesRead); } return StreamError::SUCCESS; } template mijin::Task Stream::c_readAsString(std::basic_string& outString) { static_assert(sizeof(TChar) == 1, "Can only read to 8-bit character types (char, unsigned char or char8_t"); // first try to allocate everything at once std::size_t length = 0; if (StreamError lengthError = getTotalLength(length); lengthError == StreamError::SUCCESS) { MIJIN_ASSERT(getFeatures().tell, "How did you find the length if you cannot tell()?"); length -= tell(); outString.resize(length); if (StreamError error = co_await c_readRaw(outString.data(), length); error != StreamError::SUCCESS) { co_return error; } co_return StreamError::SUCCESS; } // could not determine the size, read chunk-wise static constexpr std::size_t CHUNK_SIZE = 4096; std::array chunk; outString.clear(); while (!isAtEnd()) { std::size_t bytesRead = 0; if (StreamError error = co_await c_readRaw(chunk, true, &bytesRead); error != StreamError::SUCCESS) { co_return error; } outString.append(chunk.data(), chunk.data() + bytesRead); } co_return StreamError::SUCCESS; } inline const char* errorName(StreamError error) MIJIN_NOEXCEPT { switch (error) { case StreamError::SUCCESS: return "success"; case StreamError::IO_ERROR: return "IO error"; case StreamError::NOT_SUPPORTED: return "not supported"; case StreamError::CONNECTION_CLOSED: return "connection closed"; case StreamError::PROTOCOL_ERROR: return "protocol error"; case StreamError::WOULD_BLOCK: return "would block"; case StreamError::UNKNOWN_ERROR: return "unknown error"; case StreamError::CONNECTION_REFUSED: return "connection refused"; } return ""; } #if MIJIN_ENABLE_EXCEPTIONS // these functions don't make sense with exceptions disabled inline void throwOnError(mijin::StreamError error) { if (error == mijin::StreamError::SUCCESS) { return; } throw Exception(errorName(error)); } inline void throwOnError(mijin::StreamError error, std::string message) { if (error == mijin::StreamError::SUCCESS) { return; } throw Exception(message + ": " + errorName(error)); } template inline decltype(auto) throwOnError(StreamResult&& result) { if (result.isError()) { throw Exception(errorName(result.getError())); } else if (!result.isSuccess()) { throw Exception("result is empty"); } return *result; } template inline decltype(auto) throwOnError(StreamResult&& result, const std::string& message) { if (result.isError()) { throw Exception(message + ": " + errorName(result.getError())); } else if (!result.isSuccess()) { throw Exception(message + ": result is empty"); } return *result; } #endif // MIJIN_ENABLE_EXCEPTIONS } // namespace mijin #endif // !defined(MIJIN_IO_STREAM_HPP_INCLUDED)