diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-10-07 09:12:22 +0200 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-10-07 09:13:15 +0200 |
| commit | c87f9627efc8b612eb9b000acfcc6731cad15765 (patch) | |
| tree | 34eb4b9e70a51c2f3db3a97c2aef31ba0b139ec9 /src | |
Initial commit
Diffstat (limited to 'src')
| -rw-r--r-- | src/args.cc | 389 | ||||
| -rw-r--r-- | src/args.hh | 64 | ||||
| -rw-r--r-- | src/buffer.cc | 213 | ||||
| -rw-r--r-- | src/buffer.hh | 31 | ||||
| -rw-r--r-- | src/check.hh | 39 | ||||
| -rw-r--r-- | src/config.h.in | 1 | ||||
| -rw-r--r-- | src/io.cc | 232 | ||||
| -rw-r--r-- | src/io.hh | 51 | ||||
| -rw-r--r-- | src/line.cc | 127 | ||||
| -rw-r--r-- | src/line.hh | 37 | ||||
| -rw-r--r-- | src/main.cc | 31 | ||||
| -rw-r--r-- | src/str.cc | 53 | ||||
| -rw-r--r-- | src/str.hh | 21 | ||||
| -rw-r--r-- | src/unique_fd.cc | 9 | ||||
| -rw-r--r-- | src/unique_fd.hh | 36 |
15 files changed, 1334 insertions, 0 deletions
diff --git a/src/args.cc b/src/args.cc new file mode 100644 index 0000000..1794941 --- /dev/null +++ b/src/args.cc @@ -0,0 +1,389 @@ +#include "args.hh" + +#include <algorithm> +#include <cassert> +#include <cstddef> +#include <cstdint> +#include <format> +#include <iostream> +#include <map> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +std::string kEmpty; + +class OptionImpl : public Args::OptionArgument { + public: + enum Type : uint8_t { + NoArgument, + RequiredArgument, + OptionalArgument, + }; + + OptionImpl(Type type, char shortname, std::string longname, std::string arg, + std::string help) + : type(type), + shortname(shortname), + longname(std::move(longname)), + arg(std::move(arg)), + help(std::move(help)) {} + + const Type type; + const char shortname; + const std::string longname; + const std::string arg; + const std::string help; + + [[nodiscard]] bool is_set() const override { return is_set_; } + + [[nodiscard]] bool has_argument() const override { + return value_.has_value(); + } + + [[nodiscard]] const std::string& argument() const override { + if (value_.has_value()) + return value_.value(); + assert(false); + return kEmpty; + } + + void clear() { + is_set_ = false; + value_.reset(); + } + + void set_argument(std::string value) { + assert(type == Type::RequiredArgument || type == Type::OptionalArgument); + is_set_ = true; + value_ = std::move(value); + } + + void set_no_argument() { + assert(type == Type::NoArgument || type == Type::OptionalArgument); + is_set_ = true; + value_.reset(); + } + + private: + bool is_set_{false}; + std::optional<std::string> value_; +}; + +class ArgsImpl : public Args { + public: + explicit ArgsImpl(std::string prgname) : prgname_(std::move(prgname)) {} + + std::shared_ptr<Option> option(char shortname, std::string longname, + std::string help) override { + auto opt = std::make_shared<OptionImpl>( + OptionImpl::Type::NoArgument, shortname, std::move(longname), + /* arg */ std::string(), std::move(help)); + add(opt); + return opt; + } + + std::shared_ptr<OptionArgument> option_argument(char shortname, + std::string longname, + std::string arg, + std::string help, + bool required) override { + auto opt = std::make_shared<OptionImpl>( + required ? OptionImpl::Type::RequiredArgument + : OptionImpl::Type::OptionalArgument, + shortname, std::move(longname), std::move(arg), std::move(help)); + add(opt); + return opt; + } + + bool run(int argc, char** argv, + std::vector<std::string_view>* arguments = nullptr) override { + last_error_.clear(); + for (auto& opt : options_) { + opt->clear(); + } + + std::string_view prgname; + if (prgname_.empty()) { + if (argc > 0) + prgname = argv[0]; + } else { + prgname = prgname_; + } + + for (int a = 1; a < argc; ++a) { + assert(argv[a]); + if (argv[a][0] == '-' && argv[a][1] != '\0') { + if (argv[a][1] == '-') { + // long option + size_t eq = 2; + while (argv[a][eq] != '=' && argv[a][eq] != '\0') + ++eq; + size_t end = eq; + while (argv[a][end] != '\0') + ++end; + + if (end == 2) { + // "--", no more options signal + if (arguments) { + for (++a; a < argc; ++a) + arguments->emplace_back(argv[a]); + } + break; + } + + auto name = std::string_view(argv[a] + 2, eq - 2); + auto it = long_.find(name); + if (it == long_.end()) { + last_error_ = + std::format("{}: unrecognized option '--{}'", prgname, name); + return false; + } + auto& opt = options_[it->second]; + + if (eq < end) { + // long option with argument after equal sign + switch (opt->type) { + case OptionImpl::Type::NoArgument: + last_error_ = + std::format("{}: option '--{}' doesn't allow an argument", + prgname, name); + return false; + case OptionImpl::Type::RequiredArgument: + case OptionImpl::Type::OptionalArgument: + opt->set_argument( + std::string(argv[a] + eq + 1, end - (eq + 1))); + break; + } + } else { + switch (opt->type) { + case OptionImpl::Type::NoArgument: + case OptionImpl::Type::OptionalArgument: + opt->set_no_argument(); + break; + case OptionImpl::Type::RequiredArgument: + if (++a >= argc) { + last_error_ = std::format( + "{}: option '--{}' requires an argument", prgname, name); + return false; + } + opt->set_argument(argv[a]); + break; + } + } + } else { + // short options + char* current = argv[a] + 1; + for (; *current; ++current) { + auto it = short_.find(*current); + if (it == short_.end()) { + last_error_ = + std::format("{}: invalid option -- '{}'", prgname, *current); + return false; + } + + auto& opt = options_[it->second]; + switch (opt->type) { + case OptionImpl::Type::NoArgument: + case OptionImpl::Type::OptionalArgument: + opt->set_no_argument(); + break; + case OptionImpl::Type::RequiredArgument: + if (++a >= argc) { + last_error_ = + std::format("{}: option requires an argument -- '{}'", + prgname, *current); + return false; + } + opt->set_argument(argv[a]); + break; + } + } + } + } else { + if (arguments) + arguments->emplace_back(argv[a]); + } + } + return true; + } + + void print_error(std::ostream& out) const override { + if (last_error_.empty()) + return; + + out << last_error_ << '\n'; + } + + void print_help(std::ostream& out, uint32_t width = 79) const override { + if (options_.empty()) + return; + + uint32_t indent = 0; + const uint32_t max_need = width / 2; + std::vector<uint32_t> option_need; + for (auto const& opt : options_) { + uint32_t need; + if (opt->longname.empty()) { + need = 4; // -O + switch (opt->type) { + case OptionImpl::Type::NoArgument: + case OptionImpl::Type::OptionalArgument: + break; + case OptionImpl::Type::RequiredArgument: + need += 1 + (opt->arg.empty() ? 3 : opt->arg.size()); + break; + } + } else { + need = 8 + opt->longname.size(); // -O, --option + switch (opt->type) { + case OptionImpl::Type::NoArgument: + break; + case OptionImpl::Type::RequiredArgument: + // =ARG + need += 1 + (opt->arg.empty() ? 3 : opt->arg.size()); + break; + case OptionImpl::Type::OptionalArgument: + // [=ARG] + need += 3 + (opt->arg.empty() ? 3 : opt->arg.size()); + break; + } + } + need += 2; // margin + + option_need.emplace_back(need); + if (need <= max_need) { + indent = std::max(indent, need); + } + } + + print_wrap(out, width, /* indent */ 0, + "Mandatory arguments to long options" + " are mandatory for short options too."); + auto need_it = option_need.begin(); + for (auto const& opt : options_) { + if (opt->longname.empty()) { + out << " -" << opt->shortname; + switch (opt->type) { + case OptionImpl::Type::NoArgument: + case OptionImpl::Type::OptionalArgument: + break; + case OptionImpl::Type::RequiredArgument: + out << " " << (opt->arg.empty() ? "ARG" : opt->arg); + break; + } + } else { + if (opt->shortname != '\0') { + out << " -" << opt->shortname << ", --"; + } else { + out << " --"; + } + out << opt->longname; + switch (opt->type) { + case OptionImpl::Type::NoArgument: + break; + case OptionImpl::Type::RequiredArgument: + out << "=" << (opt->arg.empty() ? "ARG" : opt->arg); + break; + case OptionImpl::Type::OptionalArgument: + out << "=[" << (opt->arg.empty() ? "ARG" : opt->arg) << ']'; + break; + } + } + + auto need = *need_it++; + if (need > max_need) { + out << '\n'; + if (!opt->help.empty()) { + print_wrap(out, width, 0, opt->help); + } + } else { + if (opt->help.empty()) { + out << '\n'; + } else { + out << " "; // add margin, already included in need + while (need++ < indent) + out << ' '; + print_wrap(out, width, indent, opt->help); + } + } + } + } + + private: + void add(std::shared_ptr<OptionImpl> opt) { + if (opt->shortname == '\0' && opt->longname.empty()) { + assert(false); + } else { + auto idx = options_.size(); + if (opt->shortname != '\0') + short_.emplace(opt->shortname, idx); + if (!opt->longname.empty()) + long_.emplace(opt->longname, idx); + } + options_.emplace_back(std::move(opt)); + } + + static inline bool is_whitespace(char c) { return c == ' ' || c == '\t'; } + + static void print_wrap(std::ostream& out, uint32_t width, uint32_t indent, + const std::string& str) { + if (indent + str.size() <= width) { + out << str << '\n'; + return; + } + if (width <= indent || indent + 10 > width) { + out << '\n'; + out << str << '\n'; + return; + } + const std::string indent_str(indent, ' '); + const uint32_t avail = width - indent; + size_t offset = 0; + while (offset + avail < str.size()) { + uint32_t i = avail; + while (i > 0 && !is_whitespace(str[offset + i])) + --i; + if (i == 0) { + out << str.substr(offset, avail - 1); + out << "-\n"; + offset += avail - 1; + } else { + out << str.substr(offset, i); + out << '\n'; + offset += i; + } + out << indent_str; + } + out << str.substr(offset); + out << '\n'; + } + + const std::string prgname_; + std::vector<std::shared_ptr<OptionImpl>> options_; + std::map<char, size_t> short_; + std::map<std::string_view, size_t> long_; + std::string last_error_; +}; + +} // namespace + +std::shared_ptr<Args::Option> Args::option(std::string longname, + std::string help) { + return option(/* shortname */ '\0', std::move(longname), std::move(help)); +} + +std::shared_ptr<Args::OptionArgument> Args::option_argument( + std::string longname, std::string arg, std::string help, bool required) { + return option_argument(/* shortname */ '\0', std::move(longname), + std::move(arg), std::move(help), required); +} + +std::unique_ptr<Args> Args::create(std::string prgname) { + return std::make_unique<ArgsImpl>(std::move(prgname)); +} diff --git a/src/args.hh b/src/args.hh new file mode 100644 index 0000000..14f3716 --- /dev/null +++ b/src/args.hh @@ -0,0 +1,64 @@ +#ifndef ARGS_HH +#define ARGS_HH + +#include <cstdint> +#include <iosfwd> +#include <memory> +#include <string> +#include <string_view> +#include <vector> + +class Args { + public: + virtual ~Args() = default; + + class Option { + public: + virtual ~Option() = default; + + [[nodiscard]] virtual bool is_set() const = 0; + + protected: + Option() = default; + Option(Option const&) = delete; + Option& operator=(Option const&) = delete; + }; + + class OptionArgument : public Option { + public: + [[nodiscard]] virtual bool has_argument() const = 0; + [[nodiscard]] virtual const std::string& argument() const = 0; + }; + + static std::unique_ptr<Args> create(std::string prgname = std::string()); + + virtual std::shared_ptr<Option> option(char shortname, + std::string longname = std::string(), + std::string help = std::string()) = 0; + + std::shared_ptr<Option> option(std::string longname, + std::string help = std::string()); + + virtual std::shared_ptr<OptionArgument> option_argument( + char shortname, std::string longname = std::string(), + std::string arg = std::string(), std::string help = std::string(), + bool required = true) = 0; + + std::shared_ptr<OptionArgument> option_argument( + std::string longname, std::string arg = std::string(), + std::string help = std::string(), bool required = true); + + virtual bool run(int argc, char** argv, + std::vector<std::string_view>* arguments = nullptr) = 0; + + virtual void print_error(std::ostream& out) const = 0; + + virtual void print_help(std::ostream& out, uint32_t width = 79) const = 0; + + protected: + Args() = default; + Args(Args const&) = delete; + Args& operator=(Args const&) = delete; +}; + +#endif // ARGS_HH diff --git a/src/buffer.cc b/src/buffer.cc new file mode 100644 index 0000000..18913a5 --- /dev/null +++ b/src/buffer.cc @@ -0,0 +1,213 @@ +#include "buffer.hh" + +#include <algorithm> +#include <cassert> +#include <cstring> +#include <memory> +#include <utility> + +namespace { + +class FixedBuffer : public Buffer { + public: + explicit FixedBuffer(size_t size) : size_(size) {} + + void const* rptr(size_t& avail, size_t need) override { + if (rptr_ < wptr_) { + avail = wptr_ - rptr_; + } else if (rptr_ == wptr_ && !full_) { + avail = 0; + } else { + avail = (data_.get() + size_) - rptr_; + if (avail < need && rptr_ > data_.get()) { + rotate(); + return rptr(avail, need); + } + } + return rptr_; + } + + void consume(size_t size) override { + if (size == 0) + return; + if (rptr_ < wptr_) { + assert(std::cmp_greater_equal(wptr_ - rptr_, size)); + rptr_ += size; + if (rptr_ == wptr_) + reset(); + } else { + assert(rptr_ != wptr_ || full_); + assert(std::cmp_greater_equal((data_.get() + size_) - rptr_, size)); + rptr_ += size; + if (rptr_ == data_.get() + size_) { + rptr_ = data_.get(); + if (rptr_ == wptr_) + reset(); + } + } + } + + void* wptr(size_t& avail, size_t need) override { + if (wptr_ == nullptr) { + data_ = std::make_unique_for_overwrite<char[]>(size_); + rptr_ = wptr_ = data_.get(); + } + + if (wptr_ < rptr_) { + avail = rptr_ - wptr_; + } else if (rptr_ == wptr_ && full_) { + avail = 0; + } else { + avail = (data_.get() + size_) - wptr_; + if (avail < need && rptr_ > data_.get()) { + rotate(); + return wptr(avail, need); + } + } + return wptr_; + } + + void commit(size_t size) override { + if (size == 0) + return; + if (wptr_ < rptr_) { + assert(std::cmp_greater_equal(rptr_ - wptr_, size)); + wptr_ += size; + if (wptr_ == rptr_) { + full_ = true; + } + } else { + assert(rptr_ != wptr_ || !full_); + assert(std::cmp_greater_equal((data_.get() + size_) - wptr_, size)); + wptr_ += size; + if (wptr_ == data_.get() + size_) { + wptr_ = data_.get(); + if (wptr_ == rptr_) + full_ = true; + } + } + } + + [[nodiscard]] bool full() const override { return rptr_ == wptr_ && full_; } + + [[nodiscard]] bool empty() const override { return rptr_ == wptr_ && !full_; } + + private: + void reset() { + rptr_ = wptr_ = data_.get(); + full_ = false; + } + + void rotate() { + if (rptr_ < wptr_) { + size_t size = wptr_ - rptr_; + memmove(data_.get(), rptr_, size); + wptr_ = data_.get() + size; + } else { + size_t to_move = (data_.get() + size_) - rptr_; + if (wptr_ + to_move > rptr_) { + auto tmp = std::make_unique_for_overwrite<char[]>(to_move); + memcpy(tmp.get(), rptr_, to_move); + memmove(data_.get() + to_move, data_.get(), wptr_ - data_.get()); + memcpy(data_.get(), tmp.get(), to_move); + } else { + memmove(data_.get() + to_move, data_.get(), wptr_ - data_.get()); + memcpy(data_.get(), rptr_, to_move); + } + wptr_ += to_move; + } + rptr_ = data_.get(); + } + + size_t const size_; + std::unique_ptr<char[]> data_; + char* rptr_{nullptr}; + char* wptr_{nullptr}; + bool full_{false}; +}; + +class DynamicBuffer : public Buffer { + public: + DynamicBuffer(size_t start_size, size_t max_size) + : start_size_(start_size), max_size_(max_size) {} + + void const* rptr(size_t& avail, size_t /* need */) override { + avail = wptr_ - rptr_; + return rptr_; + } + + void consume(size_t size) override { + assert(std::cmp_greater_equal(wptr_ - rptr_, size)); + rptr_ += size; + if (rptr_ == wptr_) { + reset(); + } + } + + void* wptr(size_t& avail, size_t need) override { + avail = end_ - wptr_; + if (avail < need) { + if (end_ == nullptr) { + size_t size = std::min(max_size_, std::max(need, start_size_)); + data_ = std::make_unique_for_overwrite<char[]>(size); + end_ = data_.get() + size; + rptr_ = wptr_ = data_.get(); + avail = end_ - wptr_; + } else if (std::cmp_greater_equal(rptr_ - data_.get(), need - avail)) { + memmove(data_.get(), rptr_, wptr_ - rptr_); + wptr_ = data_.get() + (wptr_ - rptr_); + rptr_ = data_.get(); + avail = end_ - wptr_; + } else if (std::cmp_less(end_ - data_.get(), max_size_)) { + size_t current_size = end_ - data_.get(); + size_t new_size = std::min( + max_size_, current_size + std::max(need - avail, current_size)); + auto tmp = std::make_unique_for_overwrite<char[]>(new_size); + memcpy(tmp.get(), rptr_, wptr_ - rptr_); + end_ = tmp.get() + new_size; + wptr_ = tmp.get() + (wptr_ - rptr_); + rptr_ = tmp.get(); + data_ = std::move(tmp); + avail = end_ - wptr_; + } + } + return wptr_; + } + + void commit(size_t size) override { + assert(std::cmp_greater_equal(end_ - wptr_, size)); + wptr_ += size; + } + + [[nodiscard]] bool full() const override { + return rptr_ == data_.get() && wptr_ == end_ && + std::cmp_equal(end_ - data_.get(), max_size_); + } + + [[nodiscard]] bool empty() const override { return rptr_ == wptr_; } + + private: + void reset() { + if (std::cmp_greater(end_ - data_.get(), start_size_)) { + data_ = std::make_unique_for_overwrite<char[]>(start_size_); + } + rptr_ = wptr_ = data_.get(); + } + + size_t const start_size_; + size_t const max_size_; + std::unique_ptr<char[]> data_; + char* end_{nullptr}; + char* rptr_{nullptr}; + char* wptr_{nullptr}; +}; + +} // namespace + +std::unique_ptr<Buffer> Buffer::fixed(size_t size) { + return std::make_unique<FixedBuffer>(size); +} + +std::unique_ptr<Buffer> Buffer::dynamic(size_t start_size, size_t max_size) { + return std::make_unique<DynamicBuffer>(start_size, max_size); +} diff --git a/src/buffer.hh b/src/buffer.hh new file mode 100644 index 0000000..685cd36 --- /dev/null +++ b/src/buffer.hh @@ -0,0 +1,31 @@ +#ifndef BUFFER_HH +#define BUFFER_HH + +#include <cstddef> +#include <memory> + +class Buffer { + public: + virtual ~Buffer() = default; + + virtual void const* rptr(size_t& avail, size_t need = 1) = 0; + virtual void consume(size_t size) = 0; + + virtual void* wptr(size_t& avail, size_t need = 1) = 0; + virtual void commit(size_t size) = 0; + + [[nodiscard]] virtual bool full() const = 0; + [[nodiscard]] virtual bool empty() const = 0; + + [[nodiscard]] + static std::unique_ptr<Buffer> fixed(size_t size); + [[nodiscard]] + static std::unique_ptr<Buffer> dynamic(size_t start_size, size_t max_size); + + protected: + Buffer() = default; + Buffer(Buffer const&) = delete; + Buffer& operator=(Buffer const&) = delete; +}; + +#endif // BUFFER_HH diff --git a/src/check.hh b/src/check.hh new file mode 100644 index 0000000..91c1717 --- /dev/null +++ b/src/check.hh @@ -0,0 +1,39 @@ +#ifndef CHECK_HH +#define CHECK_HH + +#include <cstdlib> +#include <stdckdint.h> +#include <type_traits> + +namespace check { + +template <typename T> + requires std::is_arithmetic_v<T> +T add(T a, T b) { + T ret; + if (ckd_add(&ret, a, b)) + abort(); + return ret; +} + +template <typename T> + requires std::is_arithmetic_v<T> +T sub(T a, T b) { + T ret; + if (ckd_sub(&ret, a, b)) + abort(); + return ret; +} + +template <typename T> + requires std::is_arithmetic_v<T> +T mul(T a, T b) { + T ret; + if (ckd_mul(&ret, a, b)) + abort(); + return ret; +} + +} // namespace check + +#endif // CHECK_HH diff --git a/src/config.h.in b/src/config.h.in new file mode 100644 index 0000000..eaab018 --- /dev/null +++ b/src/config.h.in @@ -0,0 +1 @@ +#define VERSION "@version@" diff --git a/src/io.cc b/src/io.cc new file mode 100644 index 0000000..99c0518 --- /dev/null +++ b/src/io.cc @@ -0,0 +1,232 @@ +#include "io.hh" + +#include "unique_fd.hh" + +#include <algorithm> +#include <cerrno> +#include <cstdio> +#include <cstring> +#include <expected> +#include <fcntl.h> +#include <limits> +#include <memory> +#include <optional> +#include <string> +#include <sys/mman.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#include <utility> + +namespace io { + +namespace { + +class BasicReader : public Reader { + public: + explicit BasicReader(unique_fd fd) : fd_(std::move(fd)) {} + + [[nodiscard]] + std::expected<size_t, ReadError> read(void* dst, size_t max) override { + ssize_t ret = ::read( + fd_.get(), dst, + std::min(static_cast<size_t>(std::numeric_limits<ssize_t>::max()), + max)); + if (ret < 0) { + switch (errno) { + case EINTR: + return read(dst, max); + default: + return std::unexpected(ReadError::Error); + } + } else if (ret == 0 && max > 0) { + return std::unexpected(ReadError::Eof); + } + offset_ += ret; + return ret; + } + + [[nodiscard]] + std::expected<size_t, ReadError> skip(size_t max) override { + off_t ret; + if (sizeof(size_t) > sizeof(off_t)) { + ret = lseek( + fd_.get(), + // NOLINTNEXTLINE(bugprone-narrowing-conversions) + std::min(static_cast<size_t>(std::numeric_limits<off_t>::max()), max), + SEEK_CUR); + } else { + ret = lseek(fd_.get(), static_cast<off_t>(max), SEEK_CUR); + } + if (ret < 0) { + return std::unexpected(ReadError::Error); + } + // Don't want skip to go past (cached) file end. + if (!size_.has_value() || ret >= size_.value()) { + // When going past end, double check that it still is the end. + off_t ret2 = lseek(fd_.get(), 0, SEEK_END); + if (ret2 < 0) { + // We're screwed, but try to go back to original position and then + // return error. + size_.reset(); + lseek(fd_.get(), offset_, SEEK_SET); + return std::unexpected(ReadError::Error); + } + size_ = ret2; + if (ret >= ret2) { + auto distance = ret2 - offset_; + offset_ = ret2; + if (distance == 0 && max > 0) + return std::unexpected(ReadError::Eof); + return distance; + } + // Seek back to where we should be + if (lseek(fd_.get(), ret, SEEK_SET) < 0) { + return std::unexpected(ReadError::Error); + } + } + auto distance = ret - offset_; + offset_ = ret; + return distance; + } + + private: + unique_fd fd_; + off_t offset_{0}; + std::optional<off_t> size_; +}; + +class MemoryReader : public Reader { + public: + MemoryReader(void* ptr, size_t size) : ptr_(ptr), size_(size) {} + + [[nodiscard]] + std::expected<size_t, ReadError> read(void* dst, size_t max) override { + size_t avail = size_ - offset_; + if (avail == 0 && max > 0) + return std::unexpected(io::ReadError::Eof); + size_t ret = std::min(max, avail); + memcpy(dst, reinterpret_cast<char*>(ptr_) + offset_, ret); + offset_ += ret; + return ret; + } + + [[nodiscard]] + std::expected<size_t, ReadError> skip(size_t max) override { + size_t avail = size_ - offset_; + size_t ret = std::min(max, avail); + offset_ += ret; + return ret; + } + + protected: + void* ptr_; + size_t const size_; + + private: + size_t offset_{0}; +}; + +class MmapReader : public MemoryReader { + public: + MmapReader(unique_fd fd, void* ptr, size_t size) + : MemoryReader(ptr, size), fd_(std::move(fd)) {} + + ~MmapReader() override { munmap(ptr_, size_); } + + private: + unique_fd fd_; +}; + +class StringReader : public MemoryReader { + public: + explicit StringReader(std::string data) + : MemoryReader(nullptr, data.size()), data_(std::move(data)) { + ptr_ = data_.data(); + } + + private: + std::string data_; +}; + +} // namespace + +std::expected<size_t, ReadError> Reader::repeat_read(void* dst, size_t max) { + auto ret = read(dst, max); + if (!ret.has_value() || ret.value() == max) + return ret; + + char* d = reinterpret_cast<char*>(dst); + size_t offset = ret.value(); + while (true) { + ret = read(d + offset, max - offset); + if (!ret.has_value()) + break; + offset += ret.value(); + if (offset == max) + break; + } + return offset; +} + +std::expected<size_t, ReadError> Reader::repeat_skip(size_t max) { + auto ret = skip(max); + if (!ret.has_value() || ret.value() == max) + return ret; + + size_t offset = ret.value(); + while (true) { + ret = skip(max - offset); + if (!ret.has_value()) + break; + offset += ret.value(); + if (offset == max) + break; + } + return offset; +} + +std::expected<std::unique_ptr<Reader>, OpenError> open( + const std::string& file_path) { + return openat(AT_FDCWD, file_path); +} + +std::expected<std::unique_ptr<Reader>, OpenError> openat( + int dirfd, const std::string& file_path) { + unique_fd fd(::openat(dirfd, file_path.c_str(), O_RDONLY)); + if (fd) { + struct stat buf; + if (fstat(fd.get(), &buf) == 0) { + if (std::cmp_less_equal(buf.st_size, + std::numeric_limits<size_t>::max())) { + auto size = static_cast<size_t>(buf.st_size); + void* ptr = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd.get(), 0); + if (ptr != MAP_FAILED) { + return std::make_unique<MmapReader>(std::move(fd), ptr, size); + } + } + } + return std::make_unique<BasicReader>(std::move(fd)); + } + OpenError err; + switch (errno) { + case EINTR: + return openat(dirfd, file_path); + case EACCES: + err = OpenError::NoAccess; + break; + case ENOENT: + err = OpenError::NoSuchFile; + break; + default: + err = OpenError::Error; + break; + } + return std::unexpected(err); +} + +std::unique_ptr<Reader> memory(std::string data) { + return std::make_unique<StringReader>(std::move(data)); +} + +} // namespace io diff --git a/src/io.hh b/src/io.hh new file mode 100644 index 0000000..7c21028 --- /dev/null +++ b/src/io.hh @@ -0,0 +1,51 @@ +#ifndef IO_HH +#define IO_HH + +#include <cstddef> +#include <expected> +#include <memory> +#include <string> + +namespace io { + +enum class ReadError { + Error, + Eof, + InvalidData, // invalid data read (not used by raw file) + MaxTooSmall, // max argument needs to be bigger (not used by raw file) +}; + +enum class OpenError { + NoSuchFile, + NoAccess, + Error, +}; + +class Reader { + public: + virtual ~Reader() = default; + + [[nodiscard]] virtual std::expected<size_t, ReadError> read(void* dst, + size_t max) = 0; + [[nodiscard]] virtual std::expected<size_t, ReadError> skip(size_t max) = 0; + + [[nodiscard]] std::expected<size_t, ReadError> repeat_read(void* dst, + size_t max); + [[nodiscard]] std::expected<size_t, ReadError> repeat_skip(size_t max); + + protected: + Reader() = default; + + Reader(Reader const&) = delete; + Reader& operator=(Reader const&) = delete; +}; + +[[nodiscard]] std::expected<std::unique_ptr<Reader>, OpenError> open( + const std::string& file_path); +[[nodiscard]] std::expected<std::unique_ptr<Reader>, OpenError> openat( + int dirfd, const std::string& file_path); +[[nodiscard]] std::unique_ptr<Reader> memory(std::string data); + +} // namespace io + +#endif // IO_HH diff --git a/src/line.cc b/src/line.cc new file mode 100644 index 0000000..23370fc --- /dev/null +++ b/src/line.cc @@ -0,0 +1,127 @@ +#include "line.hh" + +#include "check.hh" + +#include <algorithm> +#include <cassert> +#include <cstdint> +#include <cstring> +#include <expected> +#include <memory> +#include <string_view> +#include <utility> + +namespace line { + +namespace { + +const char kLineTerminators[] = "\r\n"; + +class ReaderImpl : public Reader { + public: + ReaderImpl(std::unique_ptr<io::Reader> reader, size_t max_len) + : reader_(std::move(reader)), + max_len_(max_len), + buffer_(std::make_unique_for_overwrite<char[]>( + check::add(max_len, static_cast<size_t>(2)))), + rptr_(buffer_.get()), + wptr_(buffer_.get()), + search_(rptr_), + end_(buffer_.get() + check::add(max_len, static_cast<size_t>(2))) {} + + [[nodiscard]] std::expected<std::string_view, io::ReadError> read() override { + while (true) { + search_ = std::find_first_of(search_, wptr_, kLineTerminators, + kLineTerminators + 2); + if (search_ < wptr_) { + if (std::cmp_greater(search_ - rptr_, max_len_)) { + return line(max_len_, 0); + } + + size_t tlen; + if (*search_ == '\n') { + tlen = 1; + } else { + if (search_ + 1 == wptr_) { + make_space_if_needed(); + auto got = fill(); + if (!got.has_value()) { + if (got.error() == io::ReadError::Eof) { + return line(search_ - rptr_, 1); + } + return std::unexpected(got.error()); + } + } + if (search_[1] == '\n') { + tlen = 2; + } else { + tlen = 1; + } + } + return line(search_ - rptr_, tlen); + } + if (std::cmp_greater_equal(wptr_ - rptr_, max_len_)) { + return line(max_len_, 0); + } + + make_space_if_needed(); + auto got = fill(); + if (!got.has_value()) { + if (got.error() == io::ReadError::Eof && rptr_ != wptr_) { + return line(wptr_ - rptr_, 0); + } + return std::unexpected(got.error()); + } + } + } + + [[nodiscard]] uint64_t number() const override { return number_; } + + private: + std::string_view line(size_t len, size_t terminator_len) { + assert(len <= max_len_); + auto ret = std::string_view(rptr_, len); + rptr_ += len + terminator_len; + search_ = rptr_; + ++number_; + return ret; + } + + void make_space_if_needed() { + size_t free = rptr_ - buffer_.get(); + if (free == 0) + return; + size_t avail = end_ - wptr_; + if (avail > 1024) + return; + memmove(buffer_.get(), rptr_, wptr_ - rptr_); + search_ -= free; + wptr_ -= free; + rptr_ = buffer_.get(); + } + + std::expected<size_t, io::ReadError> fill() { + auto ret = reader_->read(wptr_, end_ - wptr_); + if (ret.has_value()) + wptr_ += ret.value(); + return ret; + } + + std::unique_ptr<io::Reader> reader_; + size_t const max_len_; + uint64_t number_{0}; + std::unique_ptr<char[]> buffer_; + char* rptr_; + char* wptr_; + char* search_; + char* const end_; +}; + +} // namespace + +std::unique_ptr<Reader> open(std::unique_ptr<io::Reader> reader, + size_t max_len) { + return std::make_unique<ReaderImpl>(std::move(reader), max_len); +} + +} // namespace line diff --git a/src/line.hh b/src/line.hh new file mode 100644 index 0000000..a8eeea8 --- /dev/null +++ b/src/line.hh @@ -0,0 +1,37 @@ +#ifndef LINE_HH +#define LINE_HH + +#include "io.hh" // IWYU pragma: export + +#include <cstddef> +#include <expected> +#include <memory> +#include <optional> +#include <string_view> + +namespace line { + +class Reader { + public: + virtual ~Reader() = default; + + // Returned view is only valid until next call to read. + [[nodiscard]] + virtual std::expected<std::string_view, io::ReadError> read() = 0; + // Starts at zero. Returns next line. + // So, before first read it is zero, after first read it is one. + [[nodiscard]] virtual uint64_t number() const = 0; + + protected: + Reader() = default; + + Reader(Reader const&) = delete; + Reader& operator=(Reader const&) = delete; +}; + +[[nodiscard]] std::unique_ptr<Reader> open(std::unique_ptr<io::Reader> reader, + size_t max_len = 8192); + +} // namespace line + +#endif // LINE_HH diff --git a/src/main.cc b/src/main.cc new file mode 100644 index 0000000..e66f95a --- /dev/null +++ b/src/main.cc @@ -0,0 +1,31 @@ +#include "args.hh" +#include "config.h" + +#include <iostream> + +#ifndef VERSION +# define VERSION "unknown" +#endif + +int main(int argc, char** argv) { + auto args = Args::create(); + auto opt_help = args->option('h', "help", "display this text and exit."); + auto opt_version = args->option('V', "version", "display version and exit."); + if (!args->run(argc, argv)) { + args->print_error(std::cerr); + std::cerr << "Try 'bluetooth-jukebox --help' for more information.\n"; + return 1; + } + if (opt_help->is_set()) { + std::cout << "Usage: bluetooth-jukebox [OPTION...]\n" + << "\n"; + args->print_help(std::cout); + return 0; + } + if (opt_version->is_set()) { + std::cout << "bluetooth-jukebox " << VERSION + << " written by Joel Klinghed <the_jk@spawned.biz>.\n"; + return 0; + } + return 0; +} diff --git a/src/str.cc b/src/str.cc new file mode 100644 index 0000000..44db3a6 --- /dev/null +++ b/src/str.cc @@ -0,0 +1,53 @@ +#include "str.hh" + +#include <cstddef> +#include <string_view> +#include <vector> + +namespace str { + +namespace { + +[[nodiscard]] +inline bool is_space(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; +} + +} // namespace + +void split(std::string_view str, std::vector<std::string_view>& out, + char separator, bool keep_empty) { + out.clear(); + + size_t offset = 0; + while (true) { + auto next = str.find(separator, offset); + if (next == std::string_view::npos) { + if (keep_empty || offset < str.size()) + out.push_back(str.substr(offset)); + break; + } + if (keep_empty || offset < next) + out.push_back(str.substr(offset, next - offset)); + offset = next + 1; + } +} + +std::vector<std::string_view> split(std::string_view str, char separator, + bool keep_empty) { + std::vector<std::string_view> vec; + split(str, vec, separator, keep_empty); + return vec; +} + +std::string_view trim(std::string_view str) { + size_t s = 0; + size_t e = str.size(); + while (s < e && is_space(str[s])) + ++s; + while (e > s && is_space(str[e - 1])) + --e; + return str.substr(s, e - s); +} + +} // namespace str diff --git a/src/str.hh b/src/str.hh new file mode 100644 index 0000000..e1ee549 --- /dev/null +++ b/src/str.hh @@ -0,0 +1,21 @@ +#ifndef STR_HH +#define STR_HH + +#include <string_view> +#include <vector> + +namespace str { + +void split(std::string_view str, std::vector<std::string_view>& out, + char separator = ' ', bool keep_empty = false); + +[[nodiscard]] std::vector<std::string_view> split(std::string_view str, + char separator = ' ', + bool keep_empty = false); + +[[nodiscard]] +std::string_view trim(std::string_view str); + +} // namespace str + +#endif // STR_HH diff --git a/src/unique_fd.cc b/src/unique_fd.cc new file mode 100644 index 0000000..135a449 --- /dev/null +++ b/src/unique_fd.cc @@ -0,0 +1,9 @@ +#include "unique_fd.hh" + +#include <unistd.h> + +void unique_fd::reset(int fd) { + if (fd_ != -1) + close(fd_); + fd_ = fd; +} diff --git a/src/unique_fd.hh b/src/unique_fd.hh new file mode 100644 index 0000000..2950905 --- /dev/null +++ b/src/unique_fd.hh @@ -0,0 +1,36 @@ +#ifndef UNIQUE_FD_HH +#define UNIQUE_FD_HH + +class unique_fd { + public: + constexpr unique_fd() : fd_(-1) {} + explicit constexpr unique_fd(int fd) : fd_(fd) {} + unique_fd(unique_fd& fd) = delete; + unique_fd& operator=(unique_fd& fd) = delete; + unique_fd(unique_fd&& fd) : fd_(fd.release()) {} + unique_fd& operator=(unique_fd&& fd) { + reset(fd.release()); + return *this; + } + ~unique_fd() { reset(); } + + bool operator==(unique_fd const& fd) const { return get() == fd.get(); } + bool operator!=(unique_fd const& fd) const { return get() != fd.get(); } + + int get() const { return fd_; } + explicit operator bool() const { return fd_ != -1; } + int operator*() const { return fd_; } + + int release() { + int ret = fd_; + fd_ = -1; + return ret; + } + + void reset(int fd = -1); + + private: + int fd_; +}; + +#endif // UNIQUE_FD_HH |
