summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-10-07 09:12:22 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-10-07 09:13:15 +0200
commitc87f9627efc8b612eb9b000acfcc6731cad15765 (patch)
tree34eb4b9e70a51c2f3db3a97c2aef31ba0b139ec9 /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/args.cc389
-rw-r--r--src/args.hh64
-rw-r--r--src/buffer.cc213
-rw-r--r--src/buffer.hh31
-rw-r--r--src/check.hh39
-rw-r--r--src/config.h.in1
-rw-r--r--src/io.cc232
-rw-r--r--src/io.hh51
-rw-r--r--src/line.cc127
-rw-r--r--src/line.hh37
-rw-r--r--src/main.cc31
-rw-r--r--src/str.cc53
-rw-r--r--src/str.hh21
-rw-r--r--src/unique_fd.cc9
-rw-r--r--src/unique_fd.hh36
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