From 22ef7b45a8279808926796e5ca50dbe157086742 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Mon, 19 Apr 2021 20:59:49 +0300 Subject: [PATCH] Add simple binary io library --- libs/io/CMakeLists.txt | 7 ++ libs/io/include/psemek/io/error.hpp | 24 +++++ libs/io/include/psemek/io/file_stream.hpp | 95 +++++++++++++++++ libs/io/include/psemek/io/memory_stream.hpp | 111 ++++++++++++++++++++ libs/io/include/psemek/io/stream.hpp | 29 +++++ libs/io/source/error.cpp | 16 +++ libs/io/source/file_stream.cpp | 64 +++++++++++ libs/io/source/memory_stream.cpp | 26 +++++ libs/io/source/stream.cpp | 22 ++++ libs/io/tests/file.cpp | 90 ++++++++++++++++ libs/io/tests/memory.cpp | 64 +++++++++++ 11 files changed, 548 insertions(+) create mode 100644 libs/io/CMakeLists.txt create mode 100644 libs/io/include/psemek/io/error.hpp create mode 100644 libs/io/include/psemek/io/file_stream.hpp create mode 100644 libs/io/include/psemek/io/memory_stream.hpp create mode 100644 libs/io/include/psemek/io/stream.hpp create mode 100644 libs/io/source/error.cpp create mode 100644 libs/io/source/file_stream.cpp create mode 100644 libs/io/source/memory_stream.cpp create mode 100644 libs/io/source/stream.cpp create mode 100644 libs/io/tests/file.cpp create mode 100644 libs/io/tests/memory.cpp diff --git a/libs/io/CMakeLists.txt b/libs/io/CMakeLists.txt new file mode 100644 index 00000000..dc515d92 --- /dev/null +++ b/libs/io/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE PSEMEK_IO_HEADERS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "include/*.hpp") +file(GLOB_RECURSE PSEMEK_IO_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "source/*.cpp") + +psemek_add_library(psemek-io ${PSEMEK_IO_HEADERS} ${PSEMEK_IO_SOURCES}) +target_include_directories(psemek-io PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") + +psemek_glob_tests(psemek-io tests) diff --git a/libs/io/include/psemek/io/error.hpp b/libs/io/include/psemek/io/error.hpp new file mode 100644 index 00000000..926e6c0d --- /dev/null +++ b/libs/io/include/psemek/io/error.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace psemek::io +{ + + struct null_stream + : std::exception + {}; + + struct null_istream + : null_stream + { + char const * what() const noexcept override; + }; + + struct null_ostream + : null_stream + { + char const * what() const noexcept override; + }; + +} diff --git a/libs/io/include/psemek/io/file_stream.hpp b/libs/io/include/psemek/io/file_stream.hpp new file mode 100644 index 00000000..8be3dd07 --- /dev/null +++ b/libs/io/include/psemek/io/file_stream.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include +#include + +namespace psemek::io +{ + + struct file_istream + : istream + { + file_istream() = default; + + file_istream(FILE * file) + : file_{file} + {} + + file_istream(std::filesystem::path const & path); + + file_istream(file_istream && s) + : file_{s.file_} + { + s.file_ = nullptr; + } + + file_istream & operator = (file_istream && s) + { + if (this != &s) + { + reset(); + file_ = s.file_; + s.file_ = nullptr; + } + return *this; + } + + void reset(); + + std::size_t read(char * p, std::size_t size) override; + + ~file_istream() override + { + reset(); + } + + private: + FILE * file_ = nullptr; + }; + + struct file_ostream + : ostream + { + static constexpr unsigned append = 1; + + file_ostream() = default; + + file_ostream(FILE * file) + : file_{file} + {} + + file_ostream(std::filesystem::path const & path, unsigned flags = 0); + + file_ostream(file_ostream && s) + : file_{s.file_} + { + s.file_ = nullptr; + } + + file_ostream & operator = (file_ostream && s) + { + if (this != &s) + { + reset(); + file_ = s.file_; + s.file_ = nullptr; + } + return *this; + } + + void reset(); + + std::size_t write(char const * p, std::size_t size) override; + + ~file_ostream() override + { + reset(); + } + + private: + FILE * file_ = nullptr; + }; + +} diff --git a/libs/io/include/psemek/io/memory_stream.hpp b/libs/io/include/psemek/io/memory_stream.hpp new file mode 100644 index 00000000..5bb98f02 --- /dev/null +++ b/libs/io/include/psemek/io/memory_stream.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include + +#include + +namespace psemek::io +{ + + struct memory_istream + : istream + { + memory_istream() = default; + + memory_istream(std::string_view str) + : begin_{str.data()} + , end_{str.data() + str.size()} + {} + + memory_istream(char const * begin, char const * end) + : begin_{begin} + , end_{end} + {} + + memory_istream(memory_istream && s) + : begin_{s.begin_} + , end_{s.end_} + { + s.reset(); + } + + memory_istream & operator = (memory_istream && s) + { + if (this != &s) + { + begin_ = s.begin_; + end_ = s.end_; + s.reset(); + } + + return *this; + } + + void reset() + { + begin_ = nullptr; + end_ = nullptr; + } + + void swap(memory_istream & s) + { + std::swap(begin_, s.begin_); + std::swap(end_, s.end_); + } + + std::size_t read(char * p, std::size_t size) override; + + private: + char const * begin_ = nullptr; + char const * end_ = nullptr; + }; + + struct memory_ostream + : ostream + { + memory_ostream() = default; + + memory_ostream(char * begin, char * end) + : begin_{begin} + , end_{end} + {} + + memory_ostream(memory_ostream && s) + : begin_{s.begin_} + , end_{s.end_} + { + s.reset(); + } + + memory_ostream & operator = (memory_ostream && s) + { + if (this != &s) + { + begin_ = s.begin_; + end_ = s.end_; + s.reset(); + } + + return *this; + } + + void reset() + { + begin_ = nullptr; + end_ = nullptr; + } + + void swap(memory_ostream & s) + { + std::swap(begin_, s.begin_); + std::swap(end_, s.end_); + } + + std::size_t write(char const * p, std::size_t size) override; + + private: + char * begin_ = nullptr; + char * end_ = nullptr; + }; + +} diff --git a/libs/io/include/psemek/io/stream.hpp b/libs/io/include/psemek/io/stream.hpp new file mode 100644 index 00000000..988c3bec --- /dev/null +++ b/libs/io/include/psemek/io/stream.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include +#include + +namespace psemek::io +{ + + struct istream + { + virtual std::size_t read(char * p, std::size_t size) = 0; + + virtual ~istream() {} + }; + + struct ostream + { + virtual std::size_t write(char const * p, std::size_t size) = 0; + + virtual ~ostream() {} + }; + + std::unique_ptr std_in(); + std::unique_ptr std_out(); + std::unique_ptr std_err(); + +} diff --git a/libs/io/source/error.cpp b/libs/io/source/error.cpp new file mode 100644 index 00000000..d48a167d --- /dev/null +++ b/libs/io/source/error.cpp @@ -0,0 +1,16 @@ +#include + +namespace psemek::io +{ + + char const * null_istream::what() const noexcept + { + return "Attempt to read from null input stream"; + } + + char const * null_ostream::what() const noexcept + { + return "Attempt to write to null output stream"; + } + +} diff --git a/libs/io/source/file_stream.cpp b/libs/io/source/file_stream.cpp new file mode 100644 index 00000000..1988078c --- /dev/null +++ b/libs/io/source/file_stream.cpp @@ -0,0 +1,64 @@ +#include + +#include + +namespace psemek::io +{ + + static void throw_fopen [[noreturn]] (std::filesystem::path const & path) + { + throw std::runtime_error("Failed to open " + path.native() + ": " + std::string(std::strerror(errno))); + } + + static FILE * safe_fopen(std::filesystem::path const & path, const char * mode) + { + auto f = std::fopen(path.c_str(), mode); + if (!f) throw_fopen(path); + return f; + } + + file_istream::file_istream(std::filesystem::path const & path) + : file_{safe_fopen(path.c_str(), "rb")} + {} + + void file_istream::reset() + { + if (file_) + std::fclose(file_); + file_ = nullptr; + } + + std::size_t file_istream::read(char * p, std::size_t size) + { + if (!file_) throw null_istream{}; + return std::fread(p, 1, size, file_); + } + + static char const * fopen_write_mode(unsigned flags) + { + switch (flags) + { + case 0: return "wb"; + case file_ostream::append: return "ab"; + default: throw std::runtime_error("Unknown file_ostream open flags"); + } + } + + file_ostream::file_ostream(std::filesystem::path const & path, unsigned flags) + : file_{safe_fopen(path.c_str(), fopen_write_mode(flags))} + {} + + void file_ostream::reset() + { + if (file_) + std::fclose(file_); + file_ = nullptr; + } + + std::size_t file_ostream::write(char const * p, std::size_t size) + { + if (!file_) throw null_ostream{}; + return std::fwrite(p, 1, size, file_); + } + +} diff --git a/libs/io/source/memory_stream.cpp b/libs/io/source/memory_stream.cpp new file mode 100644 index 00000000..7d0bf59a --- /dev/null +++ b/libs/io/source/memory_stream.cpp @@ -0,0 +1,26 @@ +#include + +#include + +namespace psemek::io +{ + + std::size_t memory_istream::read(char * p, std::size_t size) + { + if (!begin_) throw null_istream{}; + size = std::min(size, end_ - begin_); + std::copy(begin_, begin_ + size, p); + begin_ += size; + return size; + } + + std::size_t memory_ostream::write(char const * p, std::size_t size) + { + if (!begin_) throw null_ostream{}; + size = std::min(size, end_ - begin_); + std::copy(p, p + size, begin_); + begin_ += size; + return size; + } + +} diff --git a/libs/io/source/stream.cpp b/libs/io/source/stream.cpp new file mode 100644 index 00000000..bac3795d --- /dev/null +++ b/libs/io/source/stream.cpp @@ -0,0 +1,22 @@ +#include + +namespace psemek::io +{ + + std::unique_ptr std_in() + { + return std::make_unique(stdin); + } + + std::unique_ptr std_out() + { + return std::make_unique(stdout); + } + + std::unique_ptr std_err() + { + return std::make_unique(stderr); + } + + +} diff --git a/libs/io/tests/file.cpp b/libs/io/tests/file.cpp new file mode 100644 index 00000000..089a9551 --- /dev/null +++ b/libs/io/tests/file.cpp @@ -0,0 +1,90 @@ +#include + +#include + +#include + +using namespace psemek::io; + +test_case(io_file_null) +{ + char buffer[256]{}; + + file_istream is; + expect_throw(is.read(buffer, std::size(buffer)), null_istream); + + file_ostream os; + expect_throw(os.write(buffer, std::size(buffer)), null_ostream); +} + +static std::filesystem::path temp_file() +{ + return std::filesystem::temp_directory_path() / "psemek_test_io"; +} + +test_case(io_file_write) +{ + char const test_str[] = "Hello, world!"; + auto const path = temp_file(); + auto const length = std::size(test_str); + + { + file_ostream os(path); + expect_equal(os.write(test_str, length), length); + } + + expect(std::filesystem::exists(path)); + + char buffer_in[256]{}; + + std::ifstream is(path); + expect(is.read(buffer_in, length)); + expect_equal(std::string_view(test_str, length), std::string_view(buffer_in, length)); +} + +test_case(io_file_append) +{ + char const test_str[] = "Hello, world!"; + auto const path = temp_file(); + auto const length = std::size(test_str); + auto const half_length = length / 2; + auto const remain_length = length - half_length; + + { + file_ostream os(path); + expect_equal(os.write(test_str, half_length), half_length); + } + + expect(std::filesystem::exists(path)); + + { + file_ostream os(path, file_ostream::append); + expect_equal(os.write(test_str + half_length, remain_length), remain_length); + } + + expect(std::filesystem::exists(path)); + + char buffer_in[256]{}; + + std::ifstream is(path); + expect(is.read(buffer_in, length)); + expect_equal(std::string_view(test_str, length), std::string_view(buffer_in, length)); +} + +test_case(io_file_read) +{ + char const test_str[] = "Hello, world!"; + auto const path = temp_file(); + auto const length = std::size(test_str); + + { + std::ofstream os(path); + expect(os.write(test_str, length)); + } + + char buffer_in[256]{}; + + file_istream is(path); + expect_equal(is.read(buffer_in, length), length); + expect_equal(std::string_view(test_str, length), std::string_view(buffer_in, length)); +} diff --git a/libs/io/tests/memory.cpp b/libs/io/tests/memory.cpp new file mode 100644 index 00000000..215d75fc --- /dev/null +++ b/libs/io/tests/memory.cpp @@ -0,0 +1,64 @@ +#include + +#include + +using namespace psemek::io; + +test_case(io_memory_null) +{ + char buffer[256]{}; + + memory_istream is; + expect_throw(is.read(buffer, std::size(buffer)), null_istream); + + memory_ostream os; + expect_throw(os.write(buffer, std::size(buffer)), null_ostream); +} + +test_case(io_memory_write) +{ + char const test_str[] = "Hello, world!"; + auto const length = std::size(test_str); + char buffer_out[256]{}; + + memory_ostream os(buffer_out, buffer_out + std::size(buffer_out)); + expect_equal(os.write(test_str, length), length); + expect_equal(std::string_view(test_str, length), std::string_view(buffer_out, length)); +} + +test_case(io_memory_read) +{ + char const test_str[] = "Hello, world!"; + auto const length = std::size(test_str); + char buffer_in[256]{}; + + memory_istream is(test_str, test_str + length); + expect_equal(is.read(buffer_in, length), length); + expect_equal(std::string_view(test_str, length), std::string_view(buffer_in, length)); +} + +test_case(io_memory_write__overflow) +{ + char const test_str[] = "Hello, world!"; + char buffer_out[8]{}; + auto const length = std::size(buffer_out); + + expect_less(length, std::size(test_str)); + + memory_ostream os(buffer_out, buffer_out + length); + expect_equal(os.write(test_str, std::size(test_str)), length); + expect_equal(std::string_view(test_str, length), std::string_view(buffer_out, length)); +} + +test_case(io_memory_read__overflow) +{ + char const test_str[] = "Hello, world!"; + char buffer_in[8]{}; + auto const length = std::size(buffer_in); + + expect_less(length, std::size(test_str)); + + memory_istream is(test_str, test_str + std::size(test_str)); + expect_equal(is.read(buffer_in, length), length); + expect_equal(std::string_view(test_str, length), std::string_view(buffer_in, length)); +}