diff --git a/LibConf b/LibConf index f0593e3..fb5242b 100644 --- a/LibConf +++ b/LibConf @@ -4,6 +4,7 @@ Import('env') mijin_sources = Split(""" source/mijin/async/coroutine.cpp source/mijin/debug/symbol_info.cpp + source/mijin/io/process.cpp source/mijin/io/stream.cpp source/mijin/util/os.cpp source/mijin/types/name.cpp diff --git a/source/mijin/io/process.cpp b/source/mijin/io/process.cpp new file mode 100644 index 0000000..1521856 --- /dev/null +++ b/source/mijin/io/process.cpp @@ -0,0 +1,175 @@ + +#include "./process.hpp" + +#include +#include +#include "../util/iterators.hpp" +#include "../util/string.hpp" + +namespace mijin +{ +ProcessStream::~ProcessStream() +{ + if (handle) { + close(); + } +} + +StreamError ProcessStream::open(const char* command, FileOpenMode mode_) +{ + mode = mode_; + + const char* modeStr; // NOLINT(cppcoreguidelines-init-variables) + switch (mode_) + { + case FileOpenMode::READ: + modeStr = "r"; + break; + case FileOpenMode::WRITE: + modeStr = "w"; + break; + case FileOpenMode::READ_WRITE: + modeStr = "rw"; + break; + default: + assert(!"Unsupported mode for ProcessStream::open()"); + return StreamError::NOT_SUPPORTED; + } + handle = popen(command, modeStr); // NOLINT(cppcoreguidelines-owning-memory) + if (!handle) { + return StreamError::IO_ERROR; + } + + return StreamError::SUCCESS; +} + +int ProcessStream::close() +{ + assert(handle); + const int result = pclose(handle); + handle = nullptr; + return result; +} + +StreamError ProcessStream::readRaw(std::span buffer, bool partial, std::size_t* outBytesRead) +{ + assert(handle); + assert(mode == FileOpenMode::READ || mode == FileOpenMode::READ_WRITE); + + if (bufferedChar >= 0 && buffer.size() > 0) + { + buffer[0] = static_cast(bufferedChar); + } + + const std::size_t readBytes = std::fread(buffer.data(), 1, buffer.size(), handle); + if (std::ferror(handle)) { + return StreamError::IO_ERROR; + } + if (!partial && readBytes < buffer.size()) { + return StreamError::IO_ERROR; + } + if (outBytesRead != nullptr) + { + *outBytesRead = readBytes + (bufferedChar >= 0 ? 1 : 0); + } + bufferedChar = -1; + return StreamError::SUCCESS; +} + +StreamError ProcessStream::writeRaw(std::span buffer) +{ + assert(handle); + assert(mode == FileOpenMode::WRITE || mode == FileOpenMode::READ_WRITE); + + const std::size_t written = std::fwrite(buffer.data(), 1, buffer.size(), handle); + if (written != buffer.size() || std::ferror(handle)) { + return StreamError::IO_ERROR; + } + + return StreamError::SUCCESS; +} + +std::size_t ProcessStream::tell() +{ + assert(handle); + + return std::ftell(handle); // TODO: does this work? +} + +StreamError ProcessStream::seek(std::intptr_t pos, SeekMode seekMode) +{ + (void) pos; + (void) seekMode; + return StreamError::NOT_SUPPORTED; +} + +void ProcessStream::flush() +{ + const int result = std::fflush(handle); + assert(result == 0); +} + +bool ProcessStream::isAtEnd() +{ + assert(handle); + + if (bufferedChar >= 0) { + return false; + } + + bufferedChar = std::fgetc(handle); + if (std::feof(handle)) { + return true; + } + return false; +} + +StreamFeatures ProcessStream::getFeatures() +{ + if (handle) + { + return { + .read = (mode == FileOpenMode::READ), + .write = (mode == FileOpenMode::WRITE || mode == FileOpenMode::READ_WRITE), + .tell = true, + .seek = false + }; + } + return {}; +} + +std::string shellEscape(const std::string& arg) noexcept +{ + std::ostringstream oss; + const bool requiresQuotes = std::any_of(arg.begin(), arg.end(), [&](const char chr) { return std::isspace(chr); }); + if (requiresQuotes) { + oss << '"'; + } + for (const char chr : arg) + { + // TODO: oh Windows this will probably be different + switch (chr) + { + case '"': + case '\'': + case '\\': + case '$': + oss << '\\'; + break; + } + oss << chr; + } + if (requiresQuotes) { + oss << '"'; + } + return oss.str(); +} + +std::string makeShellCommand(const std::vector& args) noexcept +{ + using namespace mijin::pipe; + return args + | Map(&shellEscape) + | Join(" "); +} +} // namespace mijin diff --git a/source/mijin/io/process.hpp b/source/mijin/io/process.hpp new file mode 100644 index 0000000..7d8ce52 --- /dev/null +++ b/source/mijin/io/process.hpp @@ -0,0 +1,47 @@ +#pragma once + +#if !defined(MIJIN_IO_PROCESS_HPP_INCLUDED) +#define MIJIN_IO_PROCESS_HPP_INCLUDED 1 + +#include +#include "./stream.hpp" + +namespace mijin +{ +class ProcessStream : public Stream +{ +private: + std::FILE* handle = nullptr; + FileOpenMode mode; + int bufferedChar; // for isAtEnd +public: + ~ProcessStream() override; + + StreamError open(const char* command, FileOpenMode mode_); + inline StreamError open(const std::string& command, FileOpenMode mode_) { + return open(command.c_str(), mode_); + } + inline StreamError open(const std::vector& args, FileOpenMode mode_); + int close(); + [[nodiscard]] inline bool isOpen() const { return handle != nullptr; } + + // Stream overrides + StreamError readRaw(std::span buffer, bool partial = false, 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; +}; + +[[nodiscard]] std::string shellEscape(const std::string& arg) noexcept; +[[nodiscard]] std::string makeShellCommand(const std::vector& args) noexcept; + +StreamError ProcessStream::open(const std::vector& args, FileOpenMode mode_) +{ + return open(makeShellCommand(args), mode_); +} +} + +#endif // MIJIN_IO_PROCESS_HPP_INCLUDED diff --git a/source/mijin/io/stream.hpp b/source/mijin/io/stream.hpp index f8364e8..294cefa 100644 --- a/source/mijin/io/stream.hpp +++ b/source/mijin/io/stream.hpp @@ -48,10 +48,11 @@ enum class FileOpenMode READ_WRITE }; -enum class StreamError +enum class [[nodiscard]] StreamError { SUCCESS, IO_ERROR, + NOT_SUPPORTED, UNKNOWN_ERROR }; diff --git a/source/mijin/util/iterators.hpp b/source/mijin/util/iterators.hpp index 3a33743..9525e13 100644 --- a/source/mijin/util/iterators.hpp +++ b/source/mijin/util/iterators.hpp @@ -4,6 +4,7 @@ #define MIJIN_UTIL_ITERATORS_HPP_INCLUDED 1 #include +#include #include #include #include @@ -247,6 +248,94 @@ auto replace( }; } +template +struct MappingIterator +{ + using orig_value_type = typename std::iterator_traits::value_type; + using difference_type = std::ptrdiff_t; + using value_type = std::invoke_result_t; + using iterator_category = std::bidirectional_iterator_tag; // TODO? + + TIterator base; + TFunctor functor; + + MappingIterator(TIterator base_, TFunctor functor_) noexcept : base(base_), functor(std::move(functor_)) {} + MappingIterator(const MappingIterator&) noexcept = default; + MappingIterator(MappingIterator&&) noexcept = default; + + MappingIterator& operator=(const MappingIterator&) noexcept = default; + MappingIterator& operator=(MappingIterator&&) noexcept = default; + + value_type operator*() const noexcept + { + return functor(*base); + } + + MappingIterator& operator++() noexcept + { + ++base; + return *this; + } + + MappingIterator operator++(int) noexcept + { + ReplacingIterator copy(*this); + ++(*this); + return copy; + } + + MappingIterator& operator--() noexcept + { + --base; + return *this; + } + + MappingIterator operator--(int) noexcept + { + EnumeratingIterator copy(*this); + --(*this); + return copy; + } + + bool operator==(const MappingIterator& other) const noexcept + { + return functor == other.functor && base == other.base; + } + + bool operator!=(const MappingIterator& other) const noexcept + { + return !(*this == other); + } +}; + +template +struct MappingRange : RangeAdapter +{ + using value_type = typename std::iterator_traits()))>::value_type; + + RangeRef base; + TFunctor functor; + + auto begin() const noexcept + { + return MappingIterator(base.begin(), functor); + } + + auto end() const noexcept + { + return MappingIterator(base.end(), functor); + } +}; + +template +auto map(TIterable&& iterable, TFunctor&& functor) +{ + return MappingRange{ + .base = {std::forward(iterable)}, + .functor = std::forward(functor) + }; +} + template struct ChainingIterator { @@ -419,6 +508,22 @@ auto operator|(Chain2 chain2, TSecondIterable&& secondIterable) { return chain(chain2.range.range, std::forward(secondIterable)); } + +template +struct Map +{ + T functor; + + explicit Map(T functor_) : functor(std::move(functor_)) {} +}; +template +Map(T) -> Map; + +template +auto operator|(TIterable&& iterable, Map mapper) +{ + return map(std::forward(iterable), std::move(mapper.functor)); +} } } // namespace mijin diff --git a/source/mijin/util/string.hpp b/source/mijin/util/string.hpp index ab3b763..54efcc0 100644 --- a/source/mijin/util/string.hpp +++ b/source/mijin/util/string.hpp @@ -47,6 +47,22 @@ std::string join(const TRange& elements, const char* const delimiter) return oss.str(); } +namespace pipe +{ +struct Join +{ + const char* delimiter; + + explicit Join(const char* delimiter_) noexcept : delimiter(delimiter_) {} +}; + +template +auto operator|(TIterable&& iterable, const Join& joiner) +{ + return join(std::forward(iterable), joiner.delimiter); +} +} + } // namespace mijin #endif // !defined(MIJIN_UTIL_STRING_HPP_INCLUDED)