summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.cc291
-rw-r--r--src/args.hh53
-rw-r--r--src/buffer.cc236
-rw-r--r--src/buffer.hh25
-rw-r--r--src/common.hh16
-rw-r--r--src/config.cc182
-rw-r--r--src/config.hh48
-rw-r--r--src/date.cc66
-rw-r--r--src/date.hh70
-rw-r--r--src/document.cc66
-rw-r--r--src/document.hh30
-rw-r--r--src/fcgi_protocol.cc594
-rw-r--r--src/fcgi_protocol.hh191
-rw-r--r--src/file_opener.cc106
-rw-r--r--src/file_opener.hh33
-rw-r--r--src/files_finder.cc231
-rw-r--r--src/files_finder.hh51
-rw-r--r--src/geo_json.cc345
-rw-r--r--src/geo_json.hh28
-rw-r--r--src/hash_method.cc17
-rw-r--r--src/hash_method.hh23
-rw-r--r--src/hash_method_openssl.cc34
-rw-r--r--src/hasher.cc71
-rw-r--r--src/hasher.hh29
-rw-r--r--src/htmlutil.cc59
-rw-r--r--src/htmlutil.hh21
-rw-r--r--src/http_protocol.cc916
-rw-r--r--src/http_protocol.hh170
-rw-r--r--src/image.cc344
-rw-r--r--src/image.hh67
-rw-r--r--src/inet.cc112
-rw-r--r--src/inet.hh29
-rw-r--r--src/io.cc234
-rw-r--r--src/io.hh113
-rw-r--r--src/jsutil.cc72
-rw-r--r--src/jsutil.hh22
-rw-r--r--src/location.hh23
-rw-r--r--src/logger.hh57
-rw-r--r--src/logger_base.cc69
-rw-r--r--src/logger_base.hh25
-rw-r--r--src/logger_file.cc52
-rw-r--r--src/logger_null.cc20
-rw-r--r--src/logger_stdio.cc41
-rw-r--r--src/logger_syslog.cc45
-rw-r--r--src/looper.hh38
-rw-r--r--src/looper_poll.cc223
-rw-r--r--src/mime_types.cc29
-rw-r--r--src/mime_types.hh12
-rw-r--r--src/observer_list.hh148
-rw-r--r--src/pathutil.cc44
-rw-r--r--src/pathutil.hh13
-rw-r--r--src/ro_buffer.cc29
-rw-r--r--src/ro_buffer.hh23
-rw-r--r--src/rotation.hh16
-rw-r--r--src/send_file.cc45
-rw-r--r--src/send_file.hh35
-rw-r--r--src/server.cc256
-rw-r--r--src/signal_handler.cc75
-rw-r--r--src/signal_handler.hh29
-rw-r--r--src/site.cc488
-rw-r--r--src/site.hh35
-rw-r--r--src/static_files.cc113
-rw-r--r--src/static_files.hh33
-rw-r--r--src/str_buffer.cc92
-rw-r--r--src/str_buffer.hh14
-rw-r--r--src/strutil.cc211
-rw-r--r--src/strutil.hh51
-rw-r--r--src/tag.cc139
-rw-r--r--src/tag.hh42
-rw-r--r--src/task_runner.hh24
-rw-r--r--src/task_runner_looper.cc75
-rw-r--r--src/task_runner_reply.hh19
-rw-r--r--src/task_runner_thread.cc73
-rw-r--r--src/timezone.cc38
-rw-r--r--src/timezone.hh27
-rw-r--r--src/transport.cc195
-rw-r--r--src/transport.hh147
-rw-r--r--src/transport_base.cc677
-rw-r--r--src/transport_base.hh113
-rw-r--r--src/transport_fastcgi.cc707
-rw-r--r--src/transport_fastcgi.hh8
-rw-r--r--src/transport_http.cc193
-rw-r--r--src/transport_http.hh8
-rw-r--r--src/travel.cc556
-rw-r--r--src/travel.hh137
-rw-r--r--src/tz_info.cc329
-rw-r--r--src/tz_info.hh28
-rw-r--r--src/tz_str.cc265
-rw-r--r--src/tz_str.hh16
-rw-r--r--src/unique_fd.cc10
-rw-r--r--src/unique_fd.hh48
-rw-r--r--src/unique_pipe.cc47
-rw-r--r--src/unique_pipe.hh51
-rw-r--r--src/urlutil.cc138
-rw-r--r--src/urlutil.hh60
-rw-r--r--src/video.cc171
-rw-r--r--src/video.hh31
-rw-r--r--src/weak_ptr.hh42
98 files changed, 11793 insertions, 0 deletions
diff --git a/src/args.cc b/src/args.cc
new file mode 100644
index 0000000..243b284
--- /dev/null
+++ b/src/args.cc
@@ -0,0 +1,291 @@
+#include "common.hh"
+
+#include "args.hh"
+
+#include <iostream>
+#include <unordered_map>
+#include <vector>
+
+namespace {
+
+class OptionImpl : public Option {
+public:
+ OptionImpl(char short_name, std::string long_name, std::string description,
+ bool require_arg, std::string arg_description)
+ : short_name_(short_name),
+ long_name_(std::move(long_name)),
+ description_(std::move(description)),
+ require_arg_(require_arg),
+ arg_description_(std::move(arg_description)) {
+ }
+
+ bool is_set() const override { return set_; }
+
+ std::string const& arg() const override { return arg_; }
+
+ char short_name() const { return short_name_; }
+
+ std::string const& long_name() const { return long_name_; }
+
+ std::string const& description() const { return description_; }
+
+ bool require_arg() const { return require_arg_; }
+
+ std::string const& arg_description() const { return arg_description_; }
+
+ void reset() {
+ set_ = false;
+ arg_.clear();
+ }
+
+ void set() {
+ set_ = true;
+ }
+
+ void set_arg(std::string arg) {
+ arg_ = std::move(arg);
+ }
+
+private:
+ char const short_name_;
+ std::string const long_name_;
+ std::string const description_;
+ bool const require_arg_;
+ std::string const arg_description_;
+ bool set_ = false;
+ std::string arg_;
+};
+
+class ArgsImpl : public Args {
+public:
+ ArgsImpl() = default;
+
+ Option const* add_option(char short_name, std::string long_name,
+ std::string description) override {
+ prepare_option(short_name, long_name);
+ options_.push_back(std::make_unique<OptionImpl>(short_name,
+ std::move(long_name),
+ std::move(description),
+ false, std::string()));
+ return options_.back().get();
+ }
+
+ Option const* add_option_with_arg(char short_name, std::string long_name,
+ std::string description,
+ std::string arg_description) override {
+ prepare_option(short_name, long_name);
+ options_.push_back(std::make_unique<OptionImpl>(short_name,
+ std::move(long_name),
+ std::move(description),
+ true, arg_description));
+ return options_.back().get();
+ }
+
+ bool run(int argc, char** argv, std::string_view prgname, std::ostream& err,
+ std::vector<std::string>* out) override {
+ for (int a = 1; a < argc; ++a) {
+ if (argv[a][0] == '-') {
+ if (argv[a][1] == '-') {
+ if (argv[a][2] != '\0') {
+ // A long name with optional "=" argument
+ size_t len = 2;
+ while (argv[a][len] != '=' && argv[a][len])
+ ++len;
+ std::string name(argv[a] + 2, len - 2);
+ auto it = long_names_.find(name);
+ if (it == long_names_.end()) {
+ err << prgname << ": unrecognized option '--"
+ << name << "'" << std::endl;
+ return false;
+ }
+ auto* opt = options_[it->second].get();
+ opt->set();
+ if (argv[a][len]) {
+ if (opt->require_arg()) {
+ opt->set_arg(std::string(argv[a] + len + 1));
+ } else {
+ err << prgname << ": option '--"
+ << name << "' doesn't allow an argument" << std::endl;
+ return false;
+ }
+ } else {
+ if (opt->require_arg()) {
+ if (a + 1 >= argc) {
+ err << prgname << ": option '--"
+ << name << "' requires an argument" << std::endl;
+ return false;
+ } else {
+ opt->set_arg(argv[++a]);
+ }
+ }
+ }
+ continue;
+ } else {
+ // "--", all following values are arguments
+ for (++a; a < argc; ++a)
+ out->push_back(argv[a]);
+ break;
+ }
+ } else if (argv[a][1] != '\0') {
+ // One or more short names
+ for (auto* name = argv[a] + 1; *name; ++name) {
+ auto it = short_names_.find(*name);
+ if (it == short_names_.end()) {
+ err << prgname << ": invalid option -- '"
+ << *name << "'" << std::endl;
+ return false;
+ }
+ auto* opt = options_[it->second].get();
+ opt->set();
+ if (opt->require_arg()) {
+ if (a + 1 >= argc) {
+ err << prgname << ": option requires an argument"
+ << " -- '" << *name << "'" << std::endl;
+ return false;
+ } else {
+ opt->set_arg(argv[++a]);
+ }
+ }
+ }
+ continue;
+ } else {
+ // single "-", treat as argument
+ }
+ }
+
+ out->push_back(argv[a]);
+ }
+ return true;
+ }
+
+ void print_descriptions(std::ostream& out,
+ uint32_t column_width) const override {
+ uint32_t max_left = 0;
+ for (auto const& option : options_) {
+ uint32_t left = 0;
+ if (option->short_name() != '\0') {
+ if (!option->long_name().empty()) {
+ left = 6 + option->long_name().size(); // -S, --long
+ } else {
+ left = 2; // -S
+ }
+ } else if (!option->long_name().empty()) {
+ left = 2 + option->long_name().size(); // --long
+ }
+ if (option->require_arg())
+ left += 1 + option->arg_description().size(); // (=| )ARG
+ if (left > 0)
+ left += 2; // Need at least two spaces between option and desc
+ // Prefix with two spaces (either infront of option or desc)
+ left += 2;
+
+ if (left > max_left)
+ max_left = left;
+ }
+
+ uint32_t const avail_right =
+ max_left > column_width ? 0 : column_width - max_left;
+
+ if (avail_right < 20) {
+ // Fallback mode, description on its own row.
+ for (auto const& option : options_) {
+ print_option(out, *option);
+ out << '\n' << option->description() << '\n';
+ }
+ return;
+ }
+
+ // Check if all descriptions fit, justify to the right on a 80 col width
+ bool all_desc_fit = true;
+ uint32_t max_right = 0;
+ for (auto const& option : options_) {
+ uint32_t right = option->description().size();
+ if (right > avail_right) {
+ all_desc_fit = false;
+ break;
+ }
+ if (right > max_right)
+ max_right = right;
+ }
+
+ if (all_desc_fit)
+ max_left = std::max(80u, column_width) - max_right;
+
+ for (auto const& option : options_) {
+ out << " ";
+ uint32_t left = 2 + print_option(out, *option);
+ std::fill_n(std::ostreambuf_iterator<char>(out), max_left - left, ' ');
+
+ if (option->description().size() <= avail_right) {
+ out << option->description() << '\n';
+ continue;
+ }
+
+ // Wrap description
+ size_t last = 0;
+ bool first = true;
+ while (true) {
+ if (first) {
+ first = false;
+ } else {
+ std::fill_n(std::ostreambuf_iterator<char>(out), max_left, ' ');
+ }
+
+ size_t end = last + avail_right;
+ if (end >= option->description().size()) {
+ out << option->description().substr(last) << '\n';
+ break;
+ }
+ size_t space = option->description().rfind(' ', end);
+ if (space == std::string::npos || space < last) {
+ space = end;
+ }
+ out << option->description().substr(last, space - last) << '\n';
+ last = space < end ? space + 1 : end;
+ }
+ }
+ }
+
+private:
+ void prepare_option(char short_name, std::string const& long_name) {
+ if (short_name != '\0')
+ short_names_.emplace(short_name, options_.size());
+ if (!long_name.empty()) {
+ assert(long_name.find('=') == std::string::npos);
+ long_names_.emplace(long_name, options_.size());
+ }
+ }
+
+ size_t print_option(std::ostream& out, const OptionImpl& option) const {
+ bool only_short = false;
+ size_t ret = 0;
+ if (option.short_name() != '\0') {
+ out << '-' << option.short_name();
+ if (!option.long_name().empty()) {
+ out << ", --" << option.long_name();
+ ret = 6 + option.long_name().size();
+ } else {
+ ret = 2;
+ only_short = true;
+ }
+ } else if (!option.long_name().empty()) {
+ out << "--" << option.long_name();
+ ret = 2 + option.long_name().size();
+ }
+ if (option.require_arg()) {
+ out << (only_short ? ' ' : '=') << option.arg_description();
+ ret += 1 + option.arg_description().size();
+ }
+ return ret;
+ }
+
+ std::vector<std::unique_ptr<OptionImpl>> options_;
+ std::unordered_map<char, size_t> short_names_;
+ std::unordered_map<std::string, size_t> long_names_;
+};
+
+} // namespace
+
+std::unique_ptr<Args> Args::create() {
+ return std::make_unique<ArgsImpl>();
+}
diff --git a/src/args.hh b/src/args.hh
new file mode 100644
index 0000000..b581307
--- /dev/null
+++ b/src/args.hh
@@ -0,0 +1,53 @@
+#ifndef ARGS_HH
+#define ARGS_HH
+
+#include <iosfwd>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+class Option {
+public:
+ virtual ~Option() = default;
+
+ virtual bool is_set() const = 0;
+ virtual std::string const& arg() const = 0;
+
+protected:
+ Option() = default;
+ Option(Option const&) = delete;
+ Option& operator=(Option const&) = delete;
+};
+
+class Args {
+public:
+ virtual ~Args() = default;
+
+ static std::unique_ptr<Args> create();
+
+ // Returned Option is owned by Args instance.
+ virtual Option const* add_option(
+ char short_name,
+ std::string long_name,
+ std::string description) = 0;
+
+ virtual Option const* add_option_with_arg(
+ char short_name,
+ std::string long_name,
+ std::string description,
+ std::string arg_description) = 0;
+
+ virtual bool run(int argc, char** argv, std::string_view prgname,
+ std::ostream& err, std::vector<std::string>* out) = 0;
+
+ virtual void print_descriptions(std::ostream& out,
+ uint32_t column_width) 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..2412dca
--- /dev/null
+++ b/src/buffer.cc
@@ -0,0 +1,236 @@
+#include "common.hh"
+
+#include "buffer.hh"
+
+#include <algorithm>
+#include <vector>
+
+namespace {
+
+class Round : public Buffer {
+public:
+ explicit Round(size_t size)
+ : data_(std::make_unique<char[]>(size)), end_(data_.get() + size),
+ rptr_(data_.get()), wptr_(data_.get()), full_(false) {}
+
+ bool empty() const override {
+ return rptr_ == wptr_ && !full_;
+ }
+
+ bool full() const override {
+ return rptr_ == wptr_ && full_;
+ }
+
+ void clear() override {
+ rptr_ = wptr_ = data_.get();
+ full_ = false;
+ }
+
+ char const* rbuf(size_t want, size_t& avail) override {
+ if (rptr_ < wptr_) {
+ avail = wptr_ - rptr_;
+ } else if (rptr_ == wptr_ && !full_) {
+ avail = 0;
+ } else {
+ avail = end_ - rptr_;
+ if (want > avail) {
+ auto* target = data_.get();
+ for (; rptr_ < end_; ++rptr_) {
+ std::swap(*target, *rptr_);
+ ++target;
+ }
+ rptr_ = data_.get();
+ wptr_ += avail;
+ if (wptr_ == end_) {
+ assert(full_);
+ wptr_ = data_.get();
+ avail = end_ - rptr_;
+ } else {
+ avail = wptr_ - rptr_;
+ }
+ }
+ }
+ return rptr_;
+ }
+
+ void rcommit(size_t bytes) override {
+ if (bytes == 0)
+ return;
+ full_ = false;
+ assert(rptr_ < wptr_ ? rptr_ + bytes <= wptr_ : rptr_ + bytes <= end_);
+ rptr_ += bytes;
+ if (rptr_ == end_)
+ rptr_ = data_.get();
+ if (rptr_ == wptr_)
+ rptr_ = wptr_ = data_.get();
+ }
+
+ char* wbuf(size_t request, size_t& avail) override {
+ if (wptr_ < rptr_) {
+ avail = rptr_ - wptr_;
+ } else if (rptr_ == wptr_ && full_) {
+ avail = 0;
+ } else {
+ avail = end_ - wptr_;
+ if (avail < request && wptr_ != data_.get() && rptr_ != data_.get()) {
+ std::copy(rptr_, wptr_, data_.get());
+ auto size = wptr_ - rptr_;
+ wptr_ = data_.get() + size;
+ rptr_ = data_.get();
+ avail = end_ - wptr_;
+ }
+ }
+ return wptr_;
+ }
+
+ void wcommit(size_t bytes) override {
+ if (bytes == 0)
+ return;
+ assert(wptr_ < rptr_ ? wptr_ + bytes <= rptr_ : wptr_ + bytes <= end_);
+ wptr_ += bytes;
+ if (wptr_ == end_)
+ wptr_ = data_.get();
+ if (wptr_ == rptr_)
+ full_ = true;
+ }
+
+private:
+ std::unique_ptr<char[]> data_;
+ char* const end_;
+ char* rptr_;
+ char* wptr_;
+ bool full_;
+};
+
+class Growing : public Buffer {
+public:
+ Growing(size_t base_size, size_t max_size)
+ : base_size_(base_size), max_size_(max_size), data_(base_size) {
+ }
+
+ bool empty() const override {
+ return rptr_ == wptr_;
+ }
+
+ bool full() const override {
+ return rptr_ == 0 && wptr_ == max_size_;
+ }
+
+ void clear() override {
+ data_.resize(base_size_);
+ rptr_ = wptr_ = 0;
+ }
+
+ char const* rbuf(size_t, size_t& avail) override {
+ avail = wptr_ - rptr_;
+ return data_.data() + rptr_;
+ }
+
+ void rcommit(size_t bytes) override {
+ assert(rptr_ + bytes <= wptr_);
+ rptr_ += bytes;
+ if (rptr_ == wptr_)
+ rptr_ = wptr_ = 0;
+ }
+
+ char* wbuf(size_t request, size_t& avail) override {
+ avail = data_.size() - wptr_;
+ if (request > avail && rptr_ > 0) {
+ std::copy(data_.begin() + rptr_, data_.begin() + wptr_, data_.begin());
+ wptr_ -= rptr_;
+ rptr_ = 0;
+ avail = data_.size() - wptr_;
+ }
+ if (request > avail && data_.size() < max_size_) {
+ data_.resize(
+ std::min(max_size_,
+ data_.size() + std::max(request - avail,
+ (max_size_ - base_size_) / 8)));
+ avail = data_.size() - wptr_;
+ }
+ return data_.data() + wptr_;
+ }
+
+ void wcommit(size_t bytes) override {
+ assert(wptr_ + bytes <= data_.size());
+ wptr_ += bytes;
+ }
+
+private:
+ size_t const base_size_;
+ size_t const max_size_;
+ std::vector<char> data_;
+ size_t rptr_{0};
+ size_t wptr_{0};
+};
+
+class Null : public Buffer {
+public:
+ bool empty() const override {
+ return true;
+ }
+
+ char const* rbuf(size_t, size_t& avail) override {
+ avail = 0;
+ return buf_;
+ }
+
+ void rcommit(size_t bytes) override {
+ assert(bytes == 0);
+ }
+
+ bool full() const override {
+ return false;
+ }
+
+ void clear() override {}
+
+ char* wbuf(size_t, size_t& avail) override {
+ avail = sizeof(buf_);
+ return buf_;
+ }
+
+ void wcommit(size_t) override {
+ }
+
+private:
+ char buf_[4096];
+};
+
+} // namespace
+
+std::unique_ptr<Buffer> Buffer::fixed(size_t size) {
+ return std::make_unique<Round>(size);
+}
+
+std::unique_ptr<Buffer> Buffer::growing(size_t base_size, size_t max_size) {
+ return std::make_unique<Growing>(base_size, max_size);
+}
+
+std::unique_ptr<Buffer> Buffer::null() {
+ return std::make_unique<Null>();
+}
+
+size_t Buffer::write(Buffer* buf, void const* data, size_t len) {
+ assert(buf);
+ assert(data);
+ if (len == 0)
+ return 0;
+ auto* d = reinterpret_cast<char const*>(data);
+ size_t wrote = 0;
+ while (true) {
+ size_t avail;
+ auto want = len - wrote;
+ auto* ptr = buf->wbuf(want, avail);
+ if (avail == 0)
+ return wrote;
+ if (avail >= want) {
+ std::copy_n(d + wrote, want, ptr);
+ buf->wcommit(want);
+ return len;
+ }
+ std::copy_n(d + wrote, avail, ptr);
+ buf->wcommit(avail);
+ wrote += avail;
+ }
+}
diff --git a/src/buffer.hh b/src/buffer.hh
new file mode 100644
index 0000000..7f11a7c
--- /dev/null
+++ b/src/buffer.hh
@@ -0,0 +1,25 @@
+#ifndef BUFFER_HH
+#define BUFFER_HH
+
+#include "ro_buffer.hh"
+
+#include <memory>
+
+class Buffer : public RoBuffer {
+public:
+ static std::unique_ptr<Buffer> fixed(size_t size);
+ static std::unique_ptr<Buffer> growing(size_t base_size, size_t max_size);
+ // Acts as /dev/null, ie always empty, can write anything to it.
+ static std::unique_ptr<Buffer> null();
+
+ virtual bool full() const = 0;
+
+ virtual void clear() = 0;
+
+ virtual char* wbuf(size_t request, size_t& avail) = 0;
+ virtual void wcommit(size_t bytes) = 0;
+
+ static size_t write(Buffer* buf, void const* data, size_t len);
+};
+
+#endif // BUFFER_HH
diff --git a/src/common.hh b/src/common.hh
new file mode 100644
index 0000000..4eec123
--- /dev/null
+++ b/src/common.hh
@@ -0,0 +1,16 @@
+#ifndef COMMON_HH
+#define COMMON_HH
+
+#include <assert.h>
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#if HAVE_BUILTIN_UNREACHABLE
+#define NOTREACHED __builtin_unreachable()
+#else
+#define NOTREACHED abort()
+#endif
+
+#endif // COMMON_HH
diff --git a/src/config.cc b/src/config.cc
new file mode 100644
index 0000000..4d7cf52
--- /dev/null
+++ b/src/config.cc
@@ -0,0 +1,182 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "io.hh"
+#include "logger.hh"
+#include "strutil.hh"
+
+#include <errno.h>
+#include <fstream>
+#include <string.h>
+#include <unordered_map>
+#include <unordered_set>
+
+namespace {
+
+class ConfigImpl : public Config {
+public:
+ ConfigImpl()
+ : data_(), root_() {}
+
+ ConfigImpl(std::unordered_map<std::string, std::string> data,
+ std::filesystem::path root)
+ : data_(std::move(data)), root_(std::move(root)) {}
+
+ std::string_view get(std::string const& key,
+ std::string_view default_) const override {
+ auto it = data_.find(key);
+ if (it == data_.end())
+ return default_;
+ return it->second;
+ }
+
+ char const* get(std::string const& key,
+ char const* default_) const override {
+ auto it = data_.find(key);
+ if (it == data_.end())
+ return default_;
+ return it->second.c_str();
+ }
+
+ std::optional<uint64_t> get(std::string const& key, uint64_t default_)
+ const override {
+ auto* data = get(key, nullptr);
+ if (!data)
+ return default_;
+ return str::parse_uint64(data);
+ }
+
+ std::optional<uint64_t> get_size(std::string const& key, uint64_t default_)
+ const override {
+ auto* data = get(key, nullptr);
+ if (!data)
+ return default_;
+ char* end = nullptr;
+ double value = strtod(data, &end);
+ if (end == data)
+ return std::nullopt;
+ std::string_view suffix(end);
+ if (suffix == "t" || suffix == "T" || suffix == "TB" || suffix == "Tb")
+ return value * 1024 * 1024 * 1024 * 1024;
+ if (suffix == "g" || suffix == "G" || suffix == "GB" || suffix == "Gb")
+ return value * 1024 * 1024 * 1024;
+ if (suffix == "m" || suffix == "M" || suffix == "MB" || suffix == "Mb")
+ return value * 1024 * 1024;
+ if (suffix == "k" || suffix == "K" || suffix == "KB" || suffix == "Kb")
+ return value * 1024;
+ if (suffix == "b" || suffix == "B" || suffix == "")
+ return value;
+ return std::nullopt;
+ }
+
+ std::optional<double> get_duration(std::string const& key, double default_)
+ const override {
+ auto* data = get(key, nullptr);
+ if (!data)
+ return default_;
+ char* end = nullptr;
+ double value = strtod(data, &end);
+ if (end == data)
+ return std::nullopt;
+ std::string_view suffix(end);
+ if (suffix == "h" || suffix == "H")
+ return value * 60.0 * 60.0;
+ if (suffix == "m" || suffix == "M")
+ return value * 60.0;
+ if (suffix == "ms" || suffix == "MS")
+ return value / 1000.0;
+ if (suffix == "ns" || suffix == "NS")
+ return value / 1000000.0;
+ if (suffix == "s" || suffix == "S" || suffix == "")
+ return value;
+ return std::nullopt;
+ }
+
+ std::filesystem::path get_path(std::string const& key,
+ std::string_view default_) const override {
+ auto it = data_.find(key);
+ if (it == data_.end()) {
+ if (default_.empty())
+ return std::filesystem::path();
+ return root_ / default_;
+ }
+ return root_ / it->second;
+ }
+
+private:
+ std::unordered_map<std::string, std::string> const data_;
+ std::filesystem::path const root_;
+};
+
+
+inline bool is_space(char c) {
+ return c == ' ' || c == '\t';
+}
+
+void trim(std::string_view str, size_t* start, size_t* end) {
+ while (*start < *end && is_space(str[*start]))
+ ++*start;
+ while (*end > *start && is_space(str[*end - 1]))
+ --*end;
+}
+
+std::unique_ptr<Config> load(Logger* logger,
+ std::filesystem::path const& path) {
+ std::ifstream in(path);
+ if (!in.good()) {
+ logger->warn("Unable to open %s for reading: %s",
+ path.c_str(), strerror(errno));
+ return nullptr;
+ }
+ auto root = path.parent_path();
+ std::unordered_map<std::string, std::string> data;
+ std::string line;
+ unsigned long num = 0;
+ while (std::getline(in, line)) {
+ ++num;
+ if (line.empty() || line.front() == '#')
+ continue;
+ auto eq = line.find('=');
+ if (eq == std::string::npos) {
+ logger->warn("%s:%lu: Invalid line, no equal sign (=).",
+ path.c_str(), num);
+ return nullptr;
+ }
+ size_t key_start = 0;
+ size_t key_end = eq;
+ trim(line, &key_start, &key_end);
+ if (key_start == key_end) {
+ logger->warn("%s:%lu: Invalid line, no key before equal sign (=).",
+ path.c_str(), num);
+ return nullptr;
+ }
+ size_t value_start = eq + 1;
+ size_t value_end = line.size();
+ trim(line, &value_start, &value_end);
+ auto key = line.substr(key_start, key_end - key_start);
+ data[key] = line.substr(value_start, value_end - value_start);
+ }
+ if (!in.eof()) {
+ logger->warn("Error reading %s: %s",
+ path.c_str(), strerror(errno));
+ return nullptr;
+ }
+ return Config::create(std::move(data), std::move(root));
+}
+
+} // namespace
+
+std::unique_ptr<Config> Config::create(Logger* logger,
+ std::filesystem::path const& filepath) {
+ return load(logger, filepath);
+}
+
+std::unique_ptr<Config> Config::create(
+ std::unordered_map<std::string, std::string> data,
+ std::filesystem::path root) {
+ return std::make_unique<ConfigImpl>(std::move(data), std::move(root));
+}
+
+std::unique_ptr<Config> Config::create_empty() {
+ return std::make_unique<ConfigImpl>();
+}
diff --git a/src/config.hh b/src/config.hh
new file mode 100644
index 0000000..fd0f067
--- /dev/null
+++ b/src/config.hh
@@ -0,0 +1,48 @@
+#ifndef CONFIG_HH
+#define CONFIG_HH
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <stdint.h>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+
+class Logger;
+
+class Config {
+public:
+ virtual ~Config() = default;
+
+ static std::unique_ptr<Config> create(Logger* logger,
+ std::filesystem::path const& filepath);
+
+ static std::unique_ptr<Config> create(
+ std::unordered_map<std::string, std::string> data,
+ std::filesystem::path root);
+ static std::unique_ptr<Config> create_empty();
+
+ virtual std::string_view get(std::string const& key,
+ std::string_view default_) const = 0;
+ virtual char const* get(std::string const& key,
+ char const* default_) const = 0;
+ virtual std::optional<uint64_t> get(std::string const& key,
+ uint64_t default_) const = 0;
+
+ virtual std::optional<uint64_t> get_size(std::string const& key,
+ uint64_t default_) const = 0;
+
+ virtual std::optional<double> get_duration(std::string const& key,
+ double default_) const = 0;
+
+ virtual std::filesystem::path get_path(std::string const& key,
+ std::string_view default_) const = 0;
+
+protected:
+ Config() = default;
+ Config(Config const&) = delete;
+ Config& operator=(Config const&) = delete;
+};
+
+#endif // CONFIG_HH
diff --git a/src/date.cc b/src/date.cc
new file mode 100644
index 0000000..23f0ad6
--- /dev/null
+++ b/src/date.cc
@@ -0,0 +1,66 @@
+#include "common.hh"
+
+#include "date.hh"
+
+#include <pthread.h>
+#include <time.h>
+
+namespace {
+
+// localtime_r doesn't guarantee to call tzset as localtime does.
+pthread_once_t tzset_called = PTHREAD_ONCE_INIT;
+
+} // namespace
+
+Date Date::from_format(std::string const& format,
+ std::string const& str,
+ bool local_time) {
+ struct tm tm = {};
+ tm.tm_isdst = -1; // Must be -1 so that mktime figures it out by itself.
+ auto* end = strptime(str.c_str(), format.c_str(), &tm);
+ if (end && !*end)
+ return local_time ? mktime(&tm) : timegm(&tm);
+ return Date();
+}
+
+std::string Date::to_format(std::string const& format,
+ bool local_time) const {
+ if (empty() || format.empty())
+ return std::string();
+
+ if (local_time)
+ pthread_once(&tzset_called, tzset);
+
+ struct tm mem = {};
+ auto* tm = local_time ? localtime_r(&time_, &mem) : gmtime_r(&time_, &mem);
+ if (!tm)
+ return std::string();
+
+ std::string ret;
+ ret.resize(64);
+ while (true) {
+ auto len = strftime(ret.data(), ret.size(), format.c_str(), tm);
+ if (len > 0) {
+ ret.resize(len);
+ return ret;
+ }
+ ret.resize(ret.size() * 2);
+ }
+}
+
+Date Date::day(bool local_time) const {
+ if (empty())
+ return *this;
+
+ if (local_time)
+ pthread_once(&tzset_called, tzset);
+
+ struct tm tm = {};
+ auto* ret = local_time ? localtime_r(&time_, &tm) : gmtime_r(&time_, &tm);
+ if (!ret)
+ return *this;
+ ret->tm_hour = 0;
+ ret->tm_min = 0;
+ ret->tm_sec = 0;
+ return local_time ? mktime(ret) : timegm(ret);
+}
diff --git a/src/date.hh b/src/date.hh
new file mode 100644
index 0000000..8db396e
--- /dev/null
+++ b/src/date.hh
@@ -0,0 +1,70 @@
+#ifndef DATE_HH
+#define DATE_HH
+
+#include <string>
+#include <time.h>
+
+class Date {
+public:
+ Date()
+ : time_(-1) {}
+
+ Date(time_t time)
+ : time_(time) {}
+
+ Date(Date const&) = default;
+
+ Date& operator=(Date const&) = default;
+
+ bool empty() const {
+ return time_ < 0;
+ }
+
+ time_t value() const {
+ return time_;
+ }
+
+ // Return day of date (ie, set time to 00:00:00)
+ Date day(bool local_time = true) const;
+
+ bool operator==(Date const& date) const {
+ return empty() ? date.empty() : time_ == date.time_;
+ }
+
+ bool operator!=(Date const& date) const {
+ return !(*this == date);
+ }
+
+ bool operator<(Date const& date) const {
+ if (empty())
+ return !date.empty();
+ if (date.empty())
+ return false;
+ return time_ < date.time_;
+ }
+
+ bool operator<=(Date const& date) const {
+ return *this < date || *this == date;
+ }
+
+ bool operator>(Date const& date) const {
+ return !(*this <= date);
+ }
+
+ bool operator>=(Date const& date) const {
+ return !(*this < date);
+ }
+
+ // If str matches format, returns a non-empty date.
+ static Date from_format(std::string const& format,
+ std::string const& str,
+ bool local_time = true);
+
+ std::string to_format(std::string const& format,
+ bool local_time = true) const;
+
+private:
+ time_t time_;
+};
+
+#endif // DATE_HH
diff --git a/src/document.cc b/src/document.cc
new file mode 100644
index 0000000..498e50f
--- /dev/null
+++ b/src/document.cc
@@ -0,0 +1,66 @@
+#include "common.hh"
+
+#include "document.hh"
+#include "hash_method.hh"
+#include "htmlutil.hh"
+#include "tag.hh"
+
+#include <vector>
+
+namespace {
+
+class DocumentImpl : public Document {
+public:
+ explicit DocumentImpl(std::string title)
+ : html_(Tag::create("html")), head_(html_->add_tag("head")),
+ title_(head_->add_tag("title", std::move(title))),
+ body_(html_->add_tag("body")) {
+ }
+
+ void add_style(std::string rel_path) override {
+ head_->add_tag("link")
+ ->attr("rel", "stylesheet")
+ ->attr("href", std::move(rel_path));
+ }
+
+ void add_script(std::string src_path) override {
+ head_->add_tag("script")
+ ->attr("type", "text/javascript")
+ ->attr("src", std::move(src_path));
+ }
+
+ void add_script(std::unique_ptr<Tag> script) override {
+ if (!script->has_attr("type"))
+ script->attr("type", "text/javascript");
+ head_->add(std::move(script));
+ }
+
+ Tag* body() override {
+ return body_;
+ }
+
+ std::unique_ptr<Transport::Response> build(Transport* transport) override {
+ std::string data;
+ html_->render(&data);
+ auto sha256 = HashMethod::sha256();
+ sha256->update(data.data(), data.size());
+ auto etag = "\"" + sha256->finish() + "\"";
+ auto resp = transport->create_ok_data(std::move(data));
+ resp->add_header("Content-Type", "text/html; charset=utf-8");
+ resp->add_header("ETag", etag);
+ return resp;
+ }
+
+private:
+ std::unique_ptr<Tag> html_;
+ Tag* head_;
+ Tag* title_;
+ Tag* body_;
+};
+
+} // namespace
+
+std::unique_ptr<Document> Document::create(std::string title) {
+ return std::make_unique<DocumentImpl>(std::move(title));
+}
+
diff --git a/src/document.hh b/src/document.hh
new file mode 100644
index 0000000..20e22f8
--- /dev/null
+++ b/src/document.hh
@@ -0,0 +1,30 @@
+#ifndef DOCUMENT_HH
+#define DOCUMENT_HH
+
+#include "tag.hh"
+#include "transport.hh"
+
+#include <memory>
+#include <string>
+
+class Document {
+public:
+ virtual ~Document() = default;
+
+ static std::unique_ptr<Document> create(std::string title);
+
+ virtual void add_style(std::string rel_path) = 0;
+ virtual void add_script(std::string src_path) = 0;
+ virtual void add_script(std::unique_ptr<Tag> script) = 0;
+
+ virtual Tag* body() = 0;
+
+ virtual std::unique_ptr<Transport::Response> build(Transport* transport) = 0;
+
+protected:
+ Document() = default;
+ Document(Document const&) = delete;
+ Document& operator=(Document const&) = delete;
+};
+
+#endif // DOCUMENT_HH
diff --git a/src/fcgi_protocol.cc b/src/fcgi_protocol.cc
new file mode 100644
index 0000000..b2a6be1
--- /dev/null
+++ b/src/fcgi_protocol.cc
@@ -0,0 +1,594 @@
+#include "common.hh"
+
+#include "buffer.hh"
+#include "fcgi_protocol.hh"
+
+#include <algorithm>
+#include <limits>
+#include <optional>
+#include <utility>
+
+namespace fcgi {
+
+namespace {
+
+constexpr const uint8_t FCGI_VERSION_1 = 1;
+
+struct RawRecord {
+ uint8_t version;
+ uint8_t type;
+ uint8_t request_id_b1;
+ uint8_t request_id_b0;
+ uint8_t content_length_b1;
+ uint8_t content_length_b0;
+ uint8_t padding_length;
+ uint8_t reserved;
+};
+
+class RecordImpl : public Record {
+public:
+ RecordImpl(uint8_t type, uint16_t request_id, uint16_t content_length,
+ uint8_t padding_length)
+ : good_(true), type_(type), request_id_(request_id),
+ content_length_(content_length), padding_length_(padding_length) {}
+
+ RecordImpl()
+ : good_(false), type_(RecordType::UnknownType), request_id_(0),
+ content_length_(0), padding_length_(0) {}
+
+ bool good() const override {
+ return good_;
+ }
+
+ uint8_t type() const override {
+ return type_;
+ }
+
+ uint16_t request_id() const override {
+ return request_id_;
+ }
+
+ uint16_t content_length() const override {
+ return content_length_;
+ }
+
+ uint8_t padding_length() const override {
+ return padding_length_;
+ }
+
+private:
+ bool const good_;
+ uint8_t const type_;
+ uint16_t const request_id_;
+ uint16_t const content_length_;
+ uint8_t const padding_length_;
+};
+
+class BeginRequestBodyImpl : public BeginRequestBody {
+public:
+ BeginRequestBodyImpl()
+ : good_(false), role_(0), flags_(0) {}
+
+ BeginRequestBodyImpl(uint16_t role, uint8_t flags)
+ : good_(true), role_(role), flags_(flags) {
+ }
+
+ bool good() const override {
+ return good_;
+ }
+
+ uint16_t role() const override {
+ return role_;
+ }
+
+ uint8_t flags() const override {
+ return flags_;
+ }
+
+ static std::unique_ptr<BeginRequestBody> parse(uint8_t const* data,
+ size_t len) {
+ if (len != 8)
+ return std::make_unique<BeginRequestBodyImpl>();
+ return std::make_unique<BeginRequestBodyImpl>(
+ static_cast<uint16_t>(data[0]) << 8 | data[1], data[2]);
+ }
+
+private:
+ bool const good_;
+ uint16_t const role_;
+ uint8_t const flags_;
+};
+
+uint8_t calc_padding(uint16_t length) {
+ auto extra = length % 8;
+ return extra ? 8 - extra : 0;
+}
+
+class RecordBuilderImpl : public RecordBuilder {
+public:
+ RecordBuilderImpl(RecordType type, uint16_t request_id,
+ uint16_t content_length, uint8_t padding_length)
+ : type_(type), request_id_(request_id), content_length_(content_length),
+ padding_length_(padding_length) {}
+
+ RecordBuilderImpl(RecordType type, uint16_t request_id, std::string body)
+ : type_(type), request_id_(request_id), content_length_(body.size()),
+ padding_length_(calc_padding(content_length_)),
+ body_(std::move(body)) {
+ assert(body_->size() <= std::numeric_limits<uint16_t>::max());
+ }
+
+ bool build(Buffer* dst) const override {
+ size_t avail;
+ size_t need = sizeof(RawRecord) +
+ (body_ ? content_length_ + padding_length_ : 0);
+ auto* ptr = dst->wbuf(need, avail);
+ if (!build(ptr, avail))
+ return false;
+ dst->wcommit(need);
+ return true;
+ }
+
+ bool build(char* ptr, size_t avail) const override {
+ if (avail < sizeof(RawRecord) +
+ (body_ ? content_length_ + padding_length_ : 0))
+ return false;
+ auto* raw = reinterpret_cast<RawRecord*>(ptr);
+ raw->version = FCGI_VERSION_1;
+ raw->type = type_;
+ raw->request_id_b1 = request_id_ >> 8;
+ raw->request_id_b0 = request_id_ & 0xff;
+ raw->content_length_b1 = content_length_ >> 8;
+ raw->content_length_b0 = content_length_ & 0xff;
+ raw->padding_length = padding_length_;
+ if (body_) {
+ std::copy_n(body_->data(), body_->size(), ptr + sizeof(RawRecord));
+ std::fill_n(ptr + sizeof(RawRecord) + body_->size(), padding_length_,
+ '\0');
+ }
+ return true;
+ }
+
+ size_t size() const override {
+ return sizeof(RawRecord) + content_length_ + padding_length_;
+ }
+
+ bool padding(Buffer* dst) const override {
+ size_t avail;
+ auto* ptr = dst->wbuf(padding_length_, avail);
+ if (!padding(ptr, avail))
+ return false;
+ dst->wcommit(padding_length_);
+ return true;
+ }
+
+ bool padding(char* ptr, size_t avail) const override {
+ if (avail < padding_length_)
+ return false;
+ std::fill_n(ptr, padding_length_, '\0');
+ return true;
+ }
+
+private:
+ RecordType const type_;
+ uint16_t const request_id_;
+ uint16_t const content_length_;
+ uint8_t const padding_length_;
+ std::optional<std::string> const body_;
+};
+
+std::unique_ptr<Record> parse_raw(RawRecord const& raw) {
+ if (raw.version != FCGI_VERSION_1)
+ return std::make_unique<RecordImpl>();
+ return std::make_unique<RecordImpl>(
+ raw.type,
+ static_cast<uint16_t>(raw.request_id_b1) << 8 | raw.request_id_b0,
+ static_cast<uint16_t>(raw.content_length_b1) << 8 | raw.content_length_b0,
+ raw.padding_length);
+}
+
+bool parse_pair(RecordStream* stream, RoBuffer* buf,
+ std::pair<std::string, std::string>& pair) {
+ size_t avail;
+ auto* ptr = stream->rbuf(buf, 2, avail);
+ if (avail < 2)
+ return false;
+ auto* u8 = reinterpret_cast<uint8_t const*>(ptr);
+ if (u8[0] & 0x80) {
+ size_t need = 5;
+ ptr = stream->rbuf(buf, need, avail);
+ if (avail < need)
+ return false;
+ u8 = reinterpret_cast<uint8_t const*>(ptr);
+ auto name_len = static_cast<uint32_t>(u8[0] & 0x7f) << 24 |
+ static_cast<uint32_t>(u8[1]) << 16 |
+ static_cast<uint32_t>(u8[2]) << 8 |
+ u8[3];
+ if (u8[4] & 0x80) {
+ need = 8 + name_len;
+ ptr = stream->rbuf(buf, need, avail);
+ if (avail < need)
+ return false;
+ u8 = reinterpret_cast<uint8_t const*>(ptr);
+ auto value_len = static_cast<uint32_t>(u8[4] & 0x7f) << 24 |
+ static_cast<uint32_t>(u8[5]) << 16 |
+ static_cast<uint32_t>(u8[6]) << 8 |
+ u8[7];
+ need = 8 + name_len + value_len;
+ ptr = stream->rbuf(buf, need, avail);
+ if (avail < need)
+ return false;
+ pair.first.assign(ptr + 8, name_len);
+ pair.second.assign(ptr + 8 + name_len, value_len);
+ stream->rcommit(buf, need);
+ return true;
+ } else {
+ auto value_len = u8[4];
+ need = 5 + name_len + value_len;
+ ptr = stream->rbuf(buf, need, avail);
+ if (avail < need)
+ return false;
+ pair.first.assign(ptr + 5, name_len);
+ pair.second.assign(ptr + 5 + name_len, value_len);
+ stream->rcommit(buf, need);
+ return true;
+ }
+ } else if (u8[1] & 0x80) {
+ auto name_len = u8[0];
+ size_t need = 5 + name_len;
+ ptr = stream->rbuf(buf, need, avail);
+ if (avail < need)
+ return false;
+ u8 = reinterpret_cast<uint8_t const*>(ptr);
+ auto value_len = static_cast<uint32_t>(u8[1] & 0x7f) << 24 |
+ static_cast<uint32_t>(u8[2]) << 16 |
+ static_cast<uint32_t>(u8[3]) << 8 |
+ u8[4];
+ need = 5 + name_len + value_len;
+ ptr = stream->rbuf(buf, need, avail);
+ if (avail < need)
+ return false;
+ pair.first.assign(ptr + 5, name_len);
+ pair.second.assign(ptr + 5 + name_len, value_len);
+ stream->rcommit(buf, need);
+ return true;
+ } else {
+ auto name_len = u8[0];
+ auto value_len = u8[1];
+ size_t need = 2 + name_len + value_len;
+ ptr = stream->rbuf(buf, need, avail);
+ if (avail < need)
+ return false;
+ pair.first.assign(ptr + 2, name_len);
+ pair.second.assign(ptr + 2 + name_len, value_len);
+ stream->rcommit(buf, need);
+ return true;
+ }
+}
+
+class RecordStreamImpl : public RecordStream {
+public:
+ RecordStreamImpl(Record const* record, bool ended)
+ : type_(record->type()),
+ padding_(record->padding_length()),
+ left_(static_cast<size_t>(record->content_length()) + padding_),
+ leftover_(Buffer::growing(64, 8192)),
+ ended_(ended || record->content_length() == 0) {
+ check_end_of_stream();
+ }
+
+ char const* rbuf(RoBuffer* buf, size_t want, size_t& avail) override {
+ if (leftover_->empty()) {
+ size_t content_avail = left_ - padding_;
+ // Avoid want == content_avail as that might miss the padding.
+ if (want < content_avail) {
+ auto* ptr = buf->rbuf(want, avail);
+ if (avail > content_avail)
+ avail = content_avail;
+ return ptr;
+ }
+ auto* ptr = buf->rbuf(left_, avail);
+ if (avail < left_) {
+ avail = 0;
+ return nullptr;
+ }
+ Buffer::write(leftover_.get(), ptr, left_ - padding_);
+ buf->rcommit(left_);
+ left_ = 0;
+ padding_ = 0;
+ }
+ auto* ptr = leftover_->rbuf(want, avail);
+ if (left_ > 0 && want > avail) {
+ size_t tmp_avail;
+ size_t tmp_need = want - avail;
+ size_t tmp_want = tmp_need;
+ if (tmp_want >= left_ - padding_)
+ tmp_want = left_;
+ auto* tmp = buf->rbuf(tmp_want, tmp_avail);
+ if (tmp_avail < tmp_want)
+ return ptr;
+ Buffer::write(leftover_.get(), tmp, tmp_need);
+ buf->rcommit(tmp_want);
+ left_ -= tmp_want;
+ if (left_ == 0)
+ padding_ = 0;
+ ptr = leftover_->rbuf(want, avail);
+ }
+ return ptr;
+ }
+
+ void rcommit(RoBuffer* buf, size_t bytes) override {
+ if (leftover_->empty()) {
+ assert(bytes <= left_ - padding_);
+ left_ -= bytes;
+ assert(left_ >= padding_);
+ if (left_ == padding_) {
+ buf->rcommit(bytes + padding_);
+ left_ = 0;
+ padding_ = 0;
+ check_end_of_stream();
+ } else {
+ buf->rcommit(bytes);
+ }
+ } else {
+ leftover_->rcommit(bytes);
+ check_end_of_stream();
+ }
+ }
+
+ bool end_of_record() const override {
+ return left_ == 0;
+ }
+
+ bool end_of_stream() const override {
+ return end_of_stream_;
+ }
+
+ bool all_available() override {
+ if (!ended_)
+ return false;
+ return left_ == 0;
+ }
+
+ void add(Record const* record) override {
+ assert(left_ == 0);
+ assert(!ended_);
+ assert(record->type() == type_);
+
+ if (record->content_length() == 0)
+ ended_ = true;
+ padding_ = record->padding_length();
+ left_ = static_cast<size_t>(record->content_length()) + padding_;
+
+ check_end_of_stream();
+ }
+
+private:
+ void check_end_of_stream() {
+ if (end_of_stream_)
+ return;
+ if (ended_ && left_ == 0 && leftover_->empty())
+ end_of_stream_ = true;
+ }
+
+ uint8_t const type_;
+ uint8_t padding_;
+ size_t left_;
+
+ std::unique_ptr<Buffer> leftover_;
+
+ // True if zero length record was added().
+ bool ended_;
+ // True if ended_ is true and all content has been read.
+ bool end_of_stream_{false};
+};
+
+class PairImpl : public Pair {
+public:
+ PairImpl(std::string name, std::string value)
+ : good_(true), name_(std::move(name)), value_(std::move(value)) {}
+ PairImpl()
+ : good_(false) {}
+
+ bool good() const override {
+ return good_;
+ }
+
+ std::string const& name() const override {
+ return name_;
+ }
+
+ std::string const& value() const override {
+ return value_;
+ }
+
+ bool next(RecordStream* stream, RoBuffer* buf) override {
+ if (stream->end_of_stream())
+ return false;
+ std::pair<std::string, std::string> tmp;
+ if (parse_pair(stream, buf, tmp)) {
+ if (good_) {
+ name_ = std::move(tmp.first);
+ value_ = std::move(tmp.second);
+ }
+ return true;
+ }
+ if (stream->all_available()) {
+ good_ = false;
+ name_.clear();
+ value_.clear();
+ return true;
+ }
+ return false;
+ }
+
+private:
+ bool good_;
+ std::string name_;
+ std::string value_;
+};
+
+class PairBuilderImpl : public PairBuilder {
+public:
+ PairBuilderImpl() = default;
+
+ void add(std::string name, std::string value) override {
+ data_.emplace_back(std::move(name), std::move(value));
+ }
+
+ size_t size() const override {
+ size_t count = 0;
+ for (auto const& pair : data_) {
+ count += str_need(pair.first.size());
+ count += str_need(pair.second.size());
+ }
+ return count;
+ }
+
+ bool build(Buffer* buf) const override {
+ auto need = size();
+ size_t avail;
+ auto* ptr = reinterpret_cast<uint8_t*>(buf->wbuf(need, avail));
+ if (avail < need)
+ return false;
+ size_t offset = 0;
+ for (auto const& pair : data_) {
+ if (pair.first.size() < 128) {
+ ptr[offset++] = pair.first.size();
+ } else {
+ writeu32(ptr + offset, pair.first.size());
+ offset += 4;
+ }
+ if (pair.second.size() < 128) {
+ ptr[offset++] = pair.second.size();
+ } else {
+ writeu32(ptr + offset, pair.second.size());
+ offset += 4;
+ }
+ std::copy_n(pair.first.data(), pair.first.size(), ptr + offset);
+ offset += pair.first.size();
+ std::copy_n(pair.second.data(), pair.second.size(), ptr + offset);
+ offset += pair.second.size();
+ }
+ assert(offset == need);
+ buf->wcommit(offset);
+ return true;
+ }
+
+private:
+ static size_t str_need(size_t len) {
+ return len < 128 ? 1 + len : 4 + len;
+ }
+
+ static void writeu32(uint8_t* dst, uint32_t value) {
+ assert(value <= 0x7ffffffful);
+ dst[0] = (value >> 24) | 0x80;
+ dst[1] = (value >> 16) & 0xff;
+ dst[2] = (value >> 8) & 0xff;
+ dst[3] = value & 0xff;
+ }
+
+ std::vector<std::pair<std::string, std::string>> data_;
+};
+
+} // namespace
+
+std::unique_ptr<Record> Record::parse(RoBuffer* buffer) {
+ static_assert(sizeof(RawRecord) == 8);
+ size_t avail;
+ auto* ptr = buffer->rbuf(sizeof(RawRecord), avail);
+ if (avail < sizeof(RawRecord))
+ return nullptr;
+ auto ret = parse_raw(*reinterpret_cast<RawRecord const*>(ptr));
+ buffer->rcommit(sizeof(RawRecord));
+ return ret;
+}
+
+std::unique_ptr<BeginRequestBody> BeginRequestBody::parse(Record const* record,
+ RoBuffer* buffer) {
+ if (record->type() != RecordType::BeginRequest)
+ return std::make_unique<BeginRequestBodyImpl>();
+ if (record->content_length() != 8)
+ return std::make_unique<BeginRequestBodyImpl>();
+ auto need = static_cast<size_t>(record->content_length()) +
+ record->padding_length();
+ size_t avail;
+ auto* ptr = reinterpret_cast<uint8_t const*>(buffer->rbuf(need, avail));
+ if (avail < need)
+ return nullptr;
+ auto ret = BeginRequestBodyImpl::parse(ptr, record->content_length());
+ buffer->rcommit(need);
+ return ret;
+}
+
+std::unique_ptr<RecordStream> RecordStream::create_stream(
+ Record const* record) {
+ return std::make_unique<RecordStreamImpl>(record, false);
+}
+
+std::unique_ptr<RecordStream> RecordStream::create_single(
+ Record const* record) {
+ return std::make_unique<RecordStreamImpl>(record, true);
+}
+
+std::unique_ptr<Pair> Pair::start(RecordStream* stream, RoBuffer* buf) {
+ std::pair<std::string, std::string> tmp;
+ if (stream->end_of_stream())
+ return nullptr;
+ if (parse_pair(stream, buf, tmp))
+ return std::make_unique<PairImpl>(std::move(tmp.first),
+ std::move(tmp.second));
+ if (stream->all_available())
+ return std::make_unique<PairImpl>();
+ return nullptr;
+}
+
+std::unique_ptr<RecordBuilder> RecordBuilder::create(RecordType type,
+ uint16_t request_id,
+ uint16_t content_length,
+ int16_t padding_length) {
+ if (padding_length < 0)
+ padding_length = calc_padding(content_length);
+ return std::make_unique<RecordBuilderImpl>(type, request_id, content_length,
+ padding_length & 0xff);
+}
+
+std::unique_ptr<RecordBuilder> RecordBuilder::create(RecordType type,
+ uint16_t request_id,
+ std::string body) {
+ return std::make_unique<RecordBuilderImpl>(type, request_id, std::move(body));
+}
+
+std::unique_ptr<RecordBuilder> RecordBuilder::create_unknown_type(
+ uint8_t unknown_type) {
+ std::string body(8, '\0');
+ body[0] = unknown_type;
+ return create(RecordType::UnknownType, 0, std::move(body));
+}
+
+std::unique_ptr<RecordBuilder> RecordBuilder::create_begin_request(
+ uint16_t request_id, Role role, uint8_t flags) {
+ std::string body(8, '\0');
+ uint16_t tmp_role = role;
+ body[0] = tmp_role >> 8;
+ body[1] = tmp_role & 0xff;
+ body[2] = flags;
+ return create(RecordType::BeginRequest, request_id, std::move(body));
+}
+
+std::unique_ptr<RecordBuilder> RecordBuilder::create_end_request(
+ uint16_t request_id, uint32_t app_status, ProtocolStatus protocol_status) {
+ std::string body(8, '\0');
+ body[0] = app_status >> 24;
+ body[1] = (app_status >> 16) & 0xff;
+ body[2] = (app_status >> 8) & 0xff;
+ body[3] = app_status & 0xff;
+ body[4] = protocol_status;
+ return create(RecordType::EndRequest, request_id, std::move(body));
+}
+
+std::unique_ptr<PairBuilder> PairBuilder::create() {
+ return std::make_unique<PairBuilderImpl>( );
+}
+
+} // namespace fcgi
diff --git a/src/fcgi_protocol.hh b/src/fcgi_protocol.hh
new file mode 100644
index 0000000..f41c4a8
--- /dev/null
+++ b/src/fcgi_protocol.hh
@@ -0,0 +1,191 @@
+#ifndef FCGI_PROTOCOL_HH
+#define FCGI_PROTOCOL_HH
+
+#include <memory>
+#include <stdint.h>
+#include <string>
+
+class Buffer;
+class RoBuffer;
+
+namespace fcgi {
+
+enum RecordType {
+ BeginRequest = 1,
+ AbortRequest = 2,
+ EndRequest = 3,
+ Params = 4,
+ Stdin = 5,
+ Stdout = 6,
+ Stderr = 7,
+ Data = 8,
+ GetValues = 9,
+ GetValuesResult = 10,
+ UnknownType = 11,
+};
+
+enum Role {
+ Responder = 1,
+ Authorizer = 2,
+ Filter = 3,
+};
+
+enum Flags {
+ KeepConn = 1,
+};
+
+enum ProtocolStatus {
+ RequestComplete = 0,
+ CantMpxConn = 1,
+ Overloaded = 2,
+ UnknownRole = 3,
+};
+
+class Record {
+public:
+ virtual ~Record() = default;
+
+ virtual bool good() const = 0;
+
+ virtual uint8_t type() const = 0;
+ virtual uint16_t request_id() const = 0;
+ virtual uint16_t content_length() const = 0;
+ virtual uint8_t padding_length() const = 0;
+
+ static std::unique_ptr<Record> parse(RoBuffer* buffer);
+
+protected:
+ Record() = default;
+ Record(Record const&) = delete;
+ Record& operator=(Record const&) = delete;
+};
+
+class BeginRequestBody {
+public:
+ virtual ~BeginRequestBody() = default;
+
+ virtual bool good() const = 0;
+
+ virtual uint16_t role() const = 0;
+ virtual uint8_t flags() const = 0;
+
+ static std::unique_ptr<BeginRequestBody> parse(Record const* record,
+ RoBuffer* buffer);
+
+protected:
+ BeginRequestBody() = default;
+ BeginRequestBody(BeginRequestBody const&) = delete;
+ BeginRequestBody& operator=(BeginRequestBody const&) = delete;
+};
+
+class RecordStream {
+public:
+ virtual ~RecordStream() = default;
+
+ // True if all data has been read from stream.
+ virtual bool end_of_stream() const = 0;
+ // True if all data from last added record has been read.
+ virtual bool end_of_record() const = 0;
+ // True if no more data is coming, ie. what rbuf() returned last is all
+ // there will ever be.
+ virtual bool all_available() = 0;
+
+ virtual char const* rbuf(RoBuffer* buf, size_t want, size_t& avail) = 0;
+ virtual void rcommit(RoBuffer* buf, size_t bytes) = 0;
+
+ // Call when next package for stream arrives.
+ virtual void add(Record const* record) = 0;
+
+ // Create stream with record as the first package in stream.
+ static std::unique_ptr<RecordStream> create_stream(Record const* record);
+
+ // Create stream with record as the only package in stream.
+ static std::unique_ptr<RecordStream> create_single(Record const* record);
+
+protected:
+ RecordStream() = default;
+ RecordStream(RecordStream const&) = delete;
+ RecordStream& operator=(RecordStream const&) = delete;
+};
+
+class Pair {
+public:
+ virtual ~Pair() = default;
+
+ // Returns false if bad encoding or an invalid stream was found.
+ // Calling next() will NOT change a false value back to true.
+ virtual bool good() const = 0;
+ virtual std::string const& name() const = 0;
+ virtual std::string const& value() const = 0;
+
+ // Returns nullptr if stream and buf needs more data.
+ static std::unique_ptr<Pair> start(
+ RecordStream* stream, RoBuffer* buf);
+
+ // Returns false if stream and buf needs more data. In that case Pair
+ // is not modified.
+ virtual bool next(RecordStream* stream, RoBuffer* buf) = 0;
+
+protected:
+ Pair() = default;
+ Pair(Pair const&) = delete;
+ Pair& operator=(Pair const&) = delete;
+};
+
+class RecordBuilder {
+public:
+ virtual ~RecordBuilder() = default;
+
+ virtual bool build(Buffer* dst) const = 0;
+ virtual bool build(char* dst, size_t avail) const = 0;
+ virtual bool padding(Buffer* dst) const = 0;
+ virtual bool padding(char* dst, size_t avail) const = 0;
+ virtual size_t size() const = 0;
+
+ static std::unique_ptr<RecordBuilder> create(
+ RecordType type,
+ uint16_t request_id,
+ uint16_t content_length,
+ int16_t padding_length = -1);
+
+ static std::unique_ptr<RecordBuilder> create(
+ RecordType type,
+ uint16_t request_id,
+ std::string body);
+
+ static std::unique_ptr<RecordBuilder> create_unknown_type(
+ uint8_t unknown_type);
+
+ static std::unique_ptr<RecordBuilder> create_begin_request(
+ uint16_t request_id, Role role, uint8_t flags);
+
+ static std::unique_ptr<RecordBuilder> create_end_request(
+ uint16_t request_id, uint32_t app_status, ProtocolStatus protocol_status);
+
+protected:
+ RecordBuilder() = default;
+ RecordBuilder(RecordBuilder const&) = delete;
+ RecordBuilder& operator=(RecordBuilder const&) = delete;
+};
+
+class PairBuilder {
+public:
+ virtual ~PairBuilder() = default;
+
+ virtual void add(std::string name, std::string value) = 0;
+
+ virtual size_t size() const = 0;
+
+ virtual bool build(Buffer* buf) const = 0;
+
+ static std::unique_ptr<PairBuilder> create();
+
+protected:
+ PairBuilder() = default;
+ PairBuilder(PairBuilder const&) = delete;
+ PairBuilder& operator=(PairBuilder const&) = delete;
+};
+
+} // namespace fcgi
+
+#endif // FCGI_PROTOCOL_HH
diff --git a/src/file_opener.cc b/src/file_opener.cc
new file mode 100644
index 0000000..60aaa66
--- /dev/null
+++ b/src/file_opener.cc
@@ -0,0 +1,106 @@
+#include "common.hh"
+
+#include "file_opener.hh"
+#include "io.hh"
+#include "task_runner.hh"
+#include "weak_ptr.hh"
+
+#include <mutex>
+#include <unordered_map>
+
+namespace {
+
+class FileOpenerImpl : public FileOpener {
+public:
+ FileOpenerImpl(std::shared_ptr<TaskRunner> runner, size_t threads)
+ : runner_(std::move(runner)), workers_(TaskRunner::create(threads)),
+ weak_ptr_owner_(this) {}
+
+ uint32_t open(std::filesystem::path path,
+ std::function<void(uint32_t, unique_fd)> callback) override {
+ uint32_t id;
+ {
+ std::lock_guard<std::mutex> lock(jobs_mutex_);
+ while (true) {
+ id = next_id_++;
+ if (next_id_ == 0)
+ next_id_ = 1;
+ if (jobs_.find(id) == jobs_.end())
+ break;
+ }
+ jobs_[id].callback_ = std::move(callback);
+ }
+ workers_->post(std::bind(&FileOpenerImpl::do_open, this, id, path));
+ return id;
+ }
+
+ void cancel(uint32_t id) override {
+ std::lock_guard<std::mutex> lock(jobs_mutex_);
+ auto it = jobs_.find(id);
+ if (it == jobs_.end())
+ return;
+ jobs_.erase(it);
+ }
+
+private:
+ struct Job {
+ std::function<void(uint32_t, unique_fd)> callback_;
+ unique_fd fd_;
+ };
+
+ void done(uint32_t id) {
+ std::lock_guard<std::mutex> lock(jobs_mutex_);
+ auto it = jobs_.find(id);
+ if (it == jobs_.end())
+ return;
+ auto fd = std::move(it->second.fd_);
+ auto callback = std::move(it->second.callback_);
+ jobs_.erase(it);
+ callback(id, std::move(fd));
+ }
+
+ void do_open(uint32_t id, std::filesystem::path path) {
+ auto fd = io::open(path, io::open_flags::rdonly);
+ if (fd) {
+ if (!io::make_nonblocking(fd.get())) {
+ assert(false);
+ fd.reset();
+ }
+ }
+ if (fd) {
+ std::lock_guard<std::mutex> lock(jobs_mutex_);
+ auto it = jobs_.find(id);
+ if (it == jobs_.end())
+ return;
+ it->second.fd_ = std::move(fd);
+ }
+ runner_->post(
+ std::bind(&FileOpenerImpl::weak_done, weak_ptr_owner_.get(), id));
+ }
+
+ static void weak_done(std::shared_ptr<WeakPtr<FileOpenerImpl>> weak_ptr,
+ uint32_t id) {
+ auto* ptr = weak_ptr->get();
+ if (ptr)
+ ptr->done(id);
+ }
+
+ std::shared_ptr<TaskRunner> runner_;
+ std::shared_ptr<WeakPtr<FileOpenerImpl>> weak_ptr_;
+ uint32_t next_id_{1};
+
+ std::mutex jobs_mutex_;
+ std::unordered_map<uint32_t, Job> jobs_;
+
+ // It is important that workers_ is (next to) last as it blocks leftover
+ // workers in destructor so should be destroyed first.
+ std::unique_ptr<TaskRunner> workers_;
+ WeakPtrOwner<FileOpenerImpl> weak_ptr_owner_;
+};
+
+} // namespace
+
+std::unique_ptr<FileOpener> FileOpener::create(
+ std::shared_ptr<TaskRunner> runner, size_t threads) {
+ return std::make_unique<FileOpenerImpl>(std::move(runner), threads);
+}
diff --git a/src/file_opener.hh b/src/file_opener.hh
new file mode 100644
index 0000000..7b01b9f
--- /dev/null
+++ b/src/file_opener.hh
@@ -0,0 +1,33 @@
+#ifndef FILE_OPENER_HH
+#define FILE_OPENER_HH
+
+#include "unique_fd.hh"
+
+#include <filesystem>
+#include <functional>
+#include <memory>
+
+class TaskRunner;
+
+class FileOpener {
+public:
+ virtual ~FileOpener() = default;
+
+ // All callbacks are posted to runner. open() and cancel() must be called
+ // on same thread as runner "runs" on.
+ static std::unique_ptr<FileOpener> create(std::shared_ptr<TaskRunner> runner,
+ size_t threads = 1);
+
+ // Never returns 0.
+ virtual uint32_t open(std::filesystem::path path,
+ std::function<void(uint32_t, unique_fd)> callback) = 0;
+
+ virtual void cancel(uint32_t id) = 0;
+
+protected:
+ FileOpener() = default;
+ FileOpener(FileOpener const&) = delete;
+ FileOpener& operator=(FileOpener const&) = delete;
+};
+
+#endif // FILE_OPENER_HH
diff --git a/src/files_finder.cc b/src/files_finder.cc
new file mode 100644
index 0000000..fef05ba
--- /dev/null
+++ b/src/files_finder.cc
@@ -0,0 +1,231 @@
+#include "common.hh"
+
+#include "files_finder.hh"
+#include "io.hh"
+#include "logger.hh"
+#include "task_runner.hh"
+#include "unique_fd.hh"
+#include "weak_ptr.hh"
+
+#include <condition_variable>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <mutex>
+#include <optional>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+namespace {
+
+constexpr uint8_t kMaxQueued = 128;
+
+class FilesFinderImpl : public FilesFinder {
+public:
+ FilesFinderImpl(
+ std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ std::filesystem::path root,
+ Delegate* delegate,
+ size_t threads)
+ : logger_(std::move(logger)), runner_(std::move(runner)),
+ workers_(TaskRunner::create(threads)), root_(std::move(root)),
+ delegate_(delegate) {
+ workers_->post(std::bind(&FilesFinderImpl::open_root, this));
+ }
+
+private:
+ void open_root() {
+ unique_fd fd = io::open(
+ root_, io::open_flags::rdonly | io::open_flags::directory);
+ if (fd) {
+ increment_queued();
+ list_dir(std::move(fd), root_, 0);
+ } else {
+ logger_->warn("Unable to open %s: %s", root_.c_str(), strerror(errno));
+ runner_->post(std::bind(&Delegate::done, delegate_));
+ }
+ }
+
+ void open_dir(int fd, std::filesystem::path path, uint16_t depth) {
+ list_dir(unique_fd(fd), path, depth);
+ }
+
+ void list_dir(unique_fd fd, std::filesystem::path path, uint16_t depth) {
+ DIR* dh = fdopendir(fd.get());
+ if (!dh) {
+ logger_->warn("Unable to list %s: %s", path.c_str(), strerror(errno));
+ return;
+ }
+ fd.release(); // fd is now owned by dh
+ while (true) {
+ errno = 0;
+ auto* de = readdir(dh);
+ if (!de) {
+ if (errno)
+ logger_->warn("Error listing %s: %s", path.c_str(), strerror(errno));
+ break;
+ }
+ size_t namlen;
+#ifdef _DIRENT_HAVE_D_NAMLEN
+ namlen = de->d_namlen;
+#else
+ namlen = strlen(de->d_name);
+#endif
+ if (namlen == 2 && de->d_name[0] == '.' && de->d_name[1] == '.')
+ continue;
+ std::optional<bool> is_dir;
+#ifdef _DIRENT_HAVE_D_TYPE
+ switch (de->d_type) {
+ case DT_DIR:
+ is_dir = true;
+ break;
+ case DT_CHR:
+ case DT_FIFO:
+ case DT_BLK:
+ case DT_REG:
+ case DT_SOCK:
+ case DT_WHT:
+ is_dir = false;
+ break;
+ case DT_LNK:
+ case DT_UNKNOWN:
+ default:
+ break;
+ }
+#endif
+ if (!is_dir.has_value()) {
+ struct stat buf;
+ if (fstatat(dirfd(dh), de->d_name, &buf, 0)) {
+ logger_->warn("Unable to stat: %s/%s: %s",
+ path.c_str(), de->d_name, strerror(errno));
+ continue;
+ }
+ is_dir = S_ISDIR(buf.st_mode);
+ }
+
+ std::string_view name(de->d_name, namlen);
+
+ if (is_dir.value()) {
+ if (delegate_->include_dir(name, depth)) {
+ unique_fd new_fd = io::openat(
+ dirfd(dh), de->d_name,
+ io::open_flags::rdonly | io::open_flags::directory);
+ if (new_fd) {
+ increment_queued();
+ workers_->post(std::bind(&FilesFinderImpl::open_dir, this,
+ new_fd.release(), path / name,
+ depth + 1));
+ } else {
+ logger_->warn("Unable to open %s/%s: %s", path.c_str(), de->d_name,
+ strerror(errno));
+ }
+ }
+ } else {
+ if (delegate_->include_file(name, depth)) {
+ increment_active();
+ runner_->post(std::bind(
+ &FilesFinderImpl::weak_call_file,
+ weak_ptr_owner_.get(),
+ path / name));
+ }
+ }
+ }
+ closedir(dh);
+
+ decrement_queued();
+ }
+
+ void increment_queued() {
+ // Queued is used both to keep track of when done() should be called
+ // but also to max sure we don't queued too many directories as
+ // each queued entry costs one open file.
+ std::unique_lock<std::mutex> lock(queued_mutex_);
+ while (queued_ >= kMaxQueued) {
+ queued_cond_.wait(lock);
+ }
+ ++queued_;
+ }
+
+ void increment_active() {
+ std::lock_guard<std::mutex> lock(queued_mutex_);
+ ++active_;
+ }
+
+ void decrement_queued() {
+ bool notify;
+ bool post_done;
+ {
+ std::lock_guard<std::mutex> lock(queued_mutex_);
+ notify = queued_ >= kMaxQueued;
+ --queued_;
+ post_done = queued_ == 0 && active_ == 0;
+ }
+ if (notify)
+ queued_cond_.notify_one();
+ if (post_done)
+ runner_->post(std::bind(&Delegate::done, delegate_));
+ }
+
+ void decrement_active() {
+ bool post_done;
+ {
+ std::lock_guard<std::mutex> lock(queued_mutex_);
+ --active_;
+ post_done = queued_ == 0 && active_ == 0;
+ }
+ if (post_done)
+ runner_->post(std::bind(&Delegate::done, delegate_));
+ }
+
+ static void weak_call_file(std::shared_ptr<WeakPtr<FilesFinderImpl>> weak_ptr,
+ std::filesystem::path path) {
+ auto* ptr = weak_ptr->get();
+ if (ptr)
+ ptr->call_file(std::move(path));
+ }
+
+ void call_file(std::filesystem::path path) {
+ delegate_->file(std::move(path));
+ decrement_active();
+ }
+
+ std::shared_ptr<Logger> logger_;
+ std::shared_ptr<TaskRunner> runner_;
+ std::shared_ptr<TaskRunner> workers_;
+ std::filesystem::path const root_;
+ Delegate* const delegate_;
+
+ std::mutex queued_mutex_;
+ std::condition_variable queued_cond_;
+ uint8_t queued_{0};
+ size_t active_{0};
+
+ WeakPtrOwner<FilesFinderImpl> weak_ptr_owner_{this};
+};
+
+} // namespace
+
+bool FilesFinder::Delegate::include_file(std::string_view name,
+ uint16_t /* depth */) const {
+ return name.empty() || name.front() != '.';
+}
+
+bool FilesFinder::Delegate::include_dir(std::string_view name,
+ uint16_t /* depth */) const {
+ return name.empty() || name.front() != '.';
+}
+
+void FilesFinder::Delegate::done() {}
+
+std::unique_ptr<FilesFinder> FilesFinder::create(
+ std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ std::filesystem::path root,
+ Delegate* delegate,
+ size_t threads) {
+ return std::make_unique<FilesFinderImpl>(std::move(logger), std::move(runner),
+ std::move(root), delegate, threads);
+}
+
diff --git a/src/files_finder.hh b/src/files_finder.hh
new file mode 100644
index 0000000..2928efa
--- /dev/null
+++ b/src/files_finder.hh
@@ -0,0 +1,51 @@
+#ifndef FILES_FINDER_HH
+#define FILES_FINDER_HH
+
+#include <memory>
+#include <filesystem>
+
+class Logger;
+class TaskRunner;
+
+class FilesFinder {
+public:
+ class Delegate {
+ public:
+ virtual ~Delegate() = default;
+
+ // Called on any thread, default implementation
+ // returns true for files not hidden.
+ // Depth is counted from root, files in root have depth zero.
+ virtual bool include_file(std::string_view name, uint16_t depth) const;
+
+ // Called on any thread, default implementation
+ // returns true for dirs not hidden.
+ // Depth is counted from root, files in root have depth zero.
+ virtual bool include_dir(std::string_view name, uint16_t depth) const;
+
+ // Called for each file found. Called on runner.
+ virtual void file(std::filesystem::path path) = 0;
+
+ // Called after all files have been found. Called on runner.
+ // Default implementation does nothing.
+ virtual void done();
+
+ protected:
+ Delegate() = default;
+ };
+
+ virtual ~FilesFinder() = default;
+
+ static std::unique_ptr<FilesFinder> create(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ std::filesystem::path root,
+ Delegate* delegate,
+ size_t threads = 1);
+
+protected:
+ FilesFinder() = default;
+ FilesFinder(FilesFinder const&) = delete;
+ FilesFinder& operator=(FilesFinder const&) = delete;
+};
+
+#endif // FILES_HH
diff --git a/src/geo_json.cc b/src/geo_json.cc
new file mode 100644
index 0000000..3c1431b
--- /dev/null
+++ b/src/geo_json.cc
@@ -0,0 +1,345 @@
+#include "common.hh"
+
+#include "geo_json.hh"
+#include "logger.hh"
+
+#include <errno.h>
+#include <rapidjson/document.h>
+#include <rapidjson/filereadstream.h>
+#include <string.h>
+#include <vector>
+
+namespace {
+
+struct Point {
+ double x;
+ double y;
+
+ Point(double x, double y)
+ : x(x), y(y) {}
+
+ bool operator==(Point const& pt) const {
+ return x == pt.x && y == pt.y;
+ }
+
+ bool operator!=(Point const& pt) const {
+ return x != pt.x || y != pt.y;
+ }
+};
+
+// Copyright 2000 softSurfer, 2012 Dan Sunday
+// This code may be freely used and modified for any purpose
+// providing that this copyright notice is included with it.
+// SoftSurfer makes no warranty for this code, and cannot be held
+// liable for any real or imagined damage resulting from its use.
+// Users of this code must verify correctness for their application.
+inline double is_left(Point p0, Point p1, Point p2) {
+ return (p1.x - p0.x) * (p2.y - p0.y)
+ - (p2.x - p0.x) * (p1.y - p0.y);
+}
+
+int wn_pnpoly(Point pt, std::vector<Point> const& poly) {
+ int wn = 0; // the winding number counter
+
+ assert(poly.size() >= 2);
+ assert(poly.front() == poly.back());
+
+ // loop through all edges of the polygon
+ // edge from poly[i] to poly[i+1]. poly[n] == poly[0].
+ for (size_t i = 0; i < poly.size() -1 ; ++i) {
+ if (poly[i].y <= pt.y) { // start y <= pt.y
+ if (poly[i + 1].y > pt.y) // an upward crossing
+ if (is_left(poly[i], poly[i + 1], pt) > 0) // P left of edge
+ ++wn; // have a valid up intersect
+ } else { // start y > P.y (no test needed)
+ if (poly[i + 1].y <= pt.y) // a downward crossing
+ if (is_left(poly[i], poly[i + 1], pt) < 0) // P right of edge
+ --wn; // have a valid down intersect
+ }
+ }
+ return wn;
+}
+
+bool in_polygon(Point pt, std::vector<Point> const& poly) {
+ return wn_pnpoly(pt, poly) != 0;
+}
+
+class GeoJsonImpl : public GeoJson {
+public:
+ GeoJsonImpl(std::shared_ptr<Logger> logger, std::filesystem::path db)
+ : logger_(std::move(logger)), db_(std::move(db)) {}
+
+ std::optional<std::string> get_data(double lat, double lng,
+ std::string_view data) const override {
+ if (!db_.empty()) {
+ FILE* fh = fopen(db_.c_str(), "rb");
+ if (fh) {
+ rapidjson::Reader reader;
+ char buffer[1024 * 1024];
+ rapidjson::FileReadStream in(fh, buffer, sizeof(buffer));
+ Handler handler(logger_.get(), lat, lng, data);
+ reader.Parse(in, handler);
+ fclose(fh);
+ return handler.data();
+ } else {
+ logger_->warn("Unable to open %s for reading: %s",
+ db_.c_str(), strerror(errno));
+ }
+ }
+ return std::nullopt;
+ }
+
+private:
+ class Handler : public rapidjson::BaseReaderHandler<rapidjson::UTF8<>,
+ Handler> {
+ enum class Expect {
+ NONE,
+ FEATURES,
+ GEOMETRY,
+ PROPERTIES,
+ KEY_VALUE,
+ GEOMETRY_TYPE,
+ GEOMETRY_COORDINATES,
+ };
+
+ public:
+ Handler(Logger* logger, double lat, double lng, std::string_view key)
+ : logger_(logger), pt_(lng, lat), key_(key) {}
+
+ bool StartObject() {
+ if (depth_ == std::numeric_limits<uint32_t>::max())
+ return false;
+ ++depth_;
+
+ switch (expect_) {
+ case Expect::GEOMETRY:
+ geometry_ = true;
+ break;
+ case Expect::PROPERTIES:
+ properties_ = true;
+ break;
+ default:
+ break;
+ }
+ return Default();
+ }
+
+ bool Key(const char* str, rapidjson::SizeType len, bool /* copy */) {
+ expect_ = Expect::NONE;
+
+ auto key = std::string_view(str, len);
+ if (depth_ == 1) {
+ if (key == "features") {
+ expect_ = Expect::FEATURES;
+ }
+ } else if (depth_ == 2) {
+ if (features_) {
+ if (key == "properties") {
+ expect_ = Expect::PROPERTIES;
+ } else if (key == "geometry") {
+ expect_ = Expect::GEOMETRY;
+ }
+ }
+ } else if (depth_ == 3) {
+ if (properties_) {
+ if (key == key_) {
+ expect_ = Expect::KEY_VALUE;
+ }
+ } else if (geometry_) {
+ if (key == "type") {
+ expect_ = Expect::GEOMETRY_TYPE;
+ } else if (key == "coordinates") {
+ expect_ = Expect::GEOMETRY_COORDINATES;
+ }
+ }
+ }
+ return true;
+ }
+
+ bool Default() {
+ expect_ = Expect::NONE;
+ return true;
+ }
+
+ bool StartArray() {
+ if (depth_ == 1) {
+ if (expect_ == Expect::FEATURES) {
+ features_ = true;
+ }
+ } else if (depth_ == 3) {
+ if (polygon_coordinates_) {
+ polygon_coordinate_ = true;
+ } else if (list_of_polygons_) {
+ polygon_coordinates_ = true;
+ } else if (expect_ == Expect::GEOMETRY_COORDINATES) {
+ list_of_polygons_ = true;
+ }
+ }
+ return Default();
+ }
+
+ bool Int(int value) {
+ return Double(value);
+ }
+
+ bool Uint(unsigned value) {
+ return Double(value);
+ }
+
+ bool Int64(int64_t value) {
+ return Double(value);
+ }
+
+ bool Uint64(uint64_t value) {
+ return Double(value);
+ }
+
+ bool Double(double value) {
+ if (depth_ == 3 && polygon_coordinate_) {
+ coord_.push_back(value);
+ }
+ return Default();
+ }
+
+ bool String(const char* data, rapidjson::SizeType len, bool /* copy */) {
+ std::string_view str(data, len);
+ if (depth_ == 3) {
+ if (expect_ == Expect::GEOMETRY_TYPE) {
+ geometry_type_ = str;
+ } else if (expect_ == Expect::KEY_VALUE) {
+ key_value_ = str;
+ }
+ }
+ return Default();
+ }
+
+ bool EndArray(rapidjson::SizeType /* count */) {
+ if (depth_ == 1) {
+ if (features_) {
+ features_ = false;
+ }
+ } else if (depth_ == 3) {
+ if (polygon_coordinate_) {
+ coords_.emplace_back(std::move(coord_));
+ coord_.clear();
+ polygon_coordinate_ = false;
+ } else if (polygon_coordinates_) {
+ polygons_.emplace_back(std::move(coords_));
+ coords_.clear();
+ polygon_coordinates_ = false;
+ } else if (list_of_polygons_) {
+ list_of_polygons_ = false;
+ }
+ }
+ return Default();
+ }
+
+ bool EndObject(rapidjson::SizeType /* members */) {
+ if (depth_ == 0)
+ return false;
+ --depth_;
+ if (depth_ == 2) {
+ if (geometry_) {
+ geometry_ = false;
+ } else if (properties_) {
+ properties_ = false;
+ }
+ } if (depth_ == 1) {
+ if (features_) {
+ if (check_if_done())
+ return false;
+
+ key_value_.reset();
+ geometry_type_.reset();
+ coord_.clear();
+ coords_.clear();
+ polygons_.clear();
+ }
+ }
+ return Default();
+ }
+
+ std::optional<std::string> data() const {
+ return data_;
+ }
+
+ private:
+ bool check_if_done() {
+ if (geometry_type_.has_value() && geometry_type_.value() == "Polygon") {
+ if (polygons_.empty()) {
+ logger_->dbg("No polygons in Polygon");
+ return true;
+ }
+ auto exterior = get_polygon(polygons_[0]);
+ if (exterior.empty())
+ return false;
+ if (in_polygon(pt_, exterior)) {
+ bool in_hole = false;
+ for (size_t i = 1; i < polygons_.size(); ++i) {
+ auto hole = get_polygon(polygons_[i]);
+ if (hole.empty())
+ return false;
+ if (in_polygon(pt_, hole)) {
+ in_hole = true;
+ break;
+ }
+ }
+ if (!in_hole) {
+ data_ = key_value_;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ std::vector<Point> get_polygon(std::vector<std::vector<double>> const& in) {
+ std::vector<Point> out;
+ for (auto const& pair : in) {
+ if (pair.size() != 2) {
+ logger_->dbg("Coordinate of size != 2");
+ return {};
+ }
+ out.emplace_back(pair[0], pair[1]);
+ }
+ if (out.empty()) {
+ logger_->dbg("Empty LineString");
+ return {};
+ }
+ if (out.size() < 2 || out.front() != out.back()) {
+ logger_->dbg("Polygon does not start and end in same point.");
+ return {};
+ }
+ return out;
+ }
+
+ Logger* const logger_;
+ Point const pt_;
+ std::string_view const key_;
+ std::optional<std::string> data_;
+ uint32_t depth_{0};
+ Expect expect_{Expect::NONE};
+ bool features_{false};
+ bool geometry_{false};
+ bool properties_{false};
+ bool list_of_polygons_{false};
+ bool polygon_coordinates_{false};
+ bool polygon_coordinate_{false};
+
+ std::optional<std::string> key_value_;
+ std::optional<std::string> geometry_type_;
+ std::vector<std::vector<std::vector<double>>> polygons_;
+ std::vector<std::vector<double>> coords_;
+ std::vector<double> coord_;
+ };
+
+ std::shared_ptr<Logger> logger_;
+ std::filesystem::path db_;
+};
+
+} // namespace
+
+std::unique_ptr<GeoJson> GeoJson::create(std::shared_ptr<Logger> logger,
+ std::filesystem::path db) {
+ return std::make_unique<GeoJsonImpl>(std::move(logger), std::move(db));
+}
diff --git a/src/geo_json.hh b/src/geo_json.hh
new file mode 100644
index 0000000..d5f4030
--- /dev/null
+++ b/src/geo_json.hh
@@ -0,0 +1,28 @@
+#ifndef GEO_JSON_HH
+#define GEO_JSON_HH
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+
+class Logger;
+
+class GeoJson {
+public:
+ virtual ~GeoJson() = default;
+
+ static std::unique_ptr<GeoJson> create(std::shared_ptr<Logger> logger,
+ std::filesystem::path db);
+
+ virtual std::optional<std::string> get_data(double lat, double lng,
+ std::string_view data) const = 0;
+
+protected:
+ GeoJson() = default;
+ GeoJson(GeoJson const&) = delete;
+ GeoJson& operator=(GeoJson const&) = delete;
+};
+
+#endif // GEO_JSON_HH
diff --git a/src/hash_method.cc b/src/hash_method.cc
new file mode 100644
index 0000000..1eee04e
--- /dev/null
+++ b/src/hash_method.cc
@@ -0,0 +1,17 @@
+#include "common.hh"
+
+#include "hash_method.hh"
+
+std::string HashMethod::to_string(uint8_t const* data, size_t len) {
+ static const char kChar[] = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+ std::string ret;
+ ret.reserve(len * 2);
+ for (size_t i = 0; i < len; ++i) {
+ ret.push_back(kChar[data[i] >> 4]);
+ ret.push_back(kChar[data[i] & 0xf]);
+ }
+ return ret;
+}
diff --git a/src/hash_method.hh b/src/hash_method.hh
new file mode 100644
index 0000000..5a61c23
--- /dev/null
+++ b/src/hash_method.hh
@@ -0,0 +1,23 @@
+#ifndef HASH_METHOD_HH
+#define HASH_METHOD_HH
+
+#include <memory>
+#include <string>
+
+class HashMethod {
+public:
+ virtual ~HashMethod() = default;
+
+ static std::unique_ptr<HashMethod> sha256();
+
+ virtual void update(void const* data, size_t count) = 0;
+
+ virtual std::string finish() = 0;
+
+protected:
+ HashMethod() = default;
+
+ std::string to_string(uint8_t const* data, size_t len);
+};
+
+#endif // HASH_METHOD_HH
diff --git a/src/hash_method_openssl.cc b/src/hash_method_openssl.cc
new file mode 100644
index 0000000..08884cb
--- /dev/null
+++ b/src/hash_method_openssl.cc
@@ -0,0 +1,34 @@
+#include "common.hh"
+
+#include "hash_method.hh"
+
+#include <openssl/sha.h>
+
+namespace {
+
+class Sha256HashMethod : public HashMethod {
+public:
+ Sha256HashMethod() {
+ SHA256_Init(&ctx_);
+ }
+
+ void update(void const* data, size_t count) override {
+ SHA256_Update(&ctx_, data, count);
+ }
+
+ std::string finish() override {
+ uint8_t out[SHA256_DIGEST_LENGTH];
+ SHA256_Final(out, &ctx_);
+ SHA256_Init(&ctx_);
+ return to_string(out, sizeof(out));
+ }
+
+private:
+ SHA256_CTX ctx_;
+};
+
+} // namespace
+
+std::unique_ptr<HashMethod> HashMethod::sha256() {
+ return std::make_unique<Sha256HashMethod>();
+}
diff --git a/src/hasher.cc b/src/hasher.cc
new file mode 100644
index 0000000..dd94ce4
--- /dev/null
+++ b/src/hasher.cc
@@ -0,0 +1,71 @@
+#include "common.hh"
+
+#include "hash_method.hh"
+#include "hasher.hh"
+#include "io.hh"
+#include "logger.hh"
+#include "task_runner.hh"
+
+#include <errno.h>
+#include <string.h>
+
+namespace {
+
+class HasherImpl : public Hasher {
+public:
+ HasherImpl(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ size_t threads)
+ : logger_(std::move(logger)), runner_(std::move(runner)),
+ workers_(TaskRunner::create(threads)) {
+ }
+
+ void hash(std::filesystem::path path,
+ std::function<void(std::string, uint64_t)> callback) override {
+ workers_->post(std::bind(&HasherImpl::do_hash, this, path, callback));
+ }
+
+private:
+ void do_hash(std::filesystem::path path,
+ std::function<void(std::string, uint64_t)> callback) {
+ auto fd = io::open(path, io::open_flags::rdonly);
+ std::string result;
+ uint64_t size = 0;
+ if (fd) {
+ auto method = HashMethod::sha256();
+ char buffer[1 * 1024 * 1024];
+ while (true) {
+ auto got = io::read(fd.get(), buffer, sizeof(buffer));
+ if (got < 0) {
+ logger_->warn("Error reading: %s: %s", path.c_str(),
+ strerror(errno));
+ size = 0;
+ break;
+ }
+ if (got == 0) {
+ result = method->finish();
+ break;
+ }
+ size += got;
+ method->update(buffer, got);
+ }
+ } else {
+ logger_->warn("Unable to open: %s: %s", path.c_str(),
+ strerror(errno));
+ }
+ runner_->post(std::bind(callback, result, size));
+ }
+
+ std::shared_ptr<Logger> logger_;
+ std::shared_ptr<TaskRunner> runner_;
+ std::unique_ptr<TaskRunner> workers_;
+};
+
+} // namespace
+
+std::unique_ptr<Hasher> Hasher::create(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ size_t threads) {
+ return std::make_unique<HasherImpl>(std::move(logger), std::move(runner),
+ threads);
+}
diff --git a/src/hasher.hh b/src/hasher.hh
new file mode 100644
index 0000000..16b361f
--- /dev/null
+++ b/src/hasher.hh
@@ -0,0 +1,29 @@
+#ifndef HASHER_HH
+#define HASHER_HH
+
+#include <filesystem>
+#include <functional>
+#include <memory>
+
+class Logger;
+class TaskRunner;
+
+class Hasher {
+public:
+ virtual ~Hasher() = default;
+
+ static std::unique_ptr<Hasher> create(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ size_t threads = 1);
+
+ virtual void hash(std::filesystem::path path,
+ std::function<void(std::string,
+ uint64_t)> callback) = 0;
+
+protected:
+ Hasher() = default;
+ Hasher(Hasher const&) = delete;
+ Hasher& operator=(Hasher const&) = delete;
+};
+
+#endif // HASHER_HH
diff --git a/src/htmlutil.cc b/src/htmlutil.cc
new file mode 100644
index 0000000..42abc4c
--- /dev/null
+++ b/src/htmlutil.cc
@@ -0,0 +1,59 @@
+#include "common.hh"
+
+#include "htmlutil.hh"
+
+namespace html {
+
+namespace {
+
+constexpr const std::string_view kBodyChars = "&<>";
+constexpr const std::string_view kAttributeChars = "&<>\"'";
+
+} // namespace
+
+std::string escape(std::string_view in, EscapeTarget target) {
+ std::string out;
+ escape(in, &out, target);
+ return out;
+}
+
+void escape(std::string_view in, std::string* out, EscapeTarget target) {
+ std::string_view chars;
+ switch (target) {
+ case EscapeTarget::BODY:
+ chars = kBodyChars;
+ break;
+ case EscapeTarget::ATTRIBUTE:
+ chars = kAttributeChars;
+ break;
+ }
+ size_t last = 0;
+ while (true) {
+ auto next = in.find_first_of(chars, last);
+ if (next == std::string::npos) {
+ out->append(in, last);
+ break;
+ }
+ out->append(in, last, next - last);
+ switch (in[next]) {
+ case '&':
+ out->append("&amp;");
+ break;
+ case '<':
+ out->append("&lt;");
+ break;
+ case '>':
+ out->append("&gt;");
+ break;
+ case '"':
+ out->append("&quot;");
+ break;
+ case '\'':
+ out->append("&apos;");
+ break;
+ }
+ last = next + 1;
+ }
+}
+
+} // namespace html
diff --git a/src/htmlutil.hh b/src/htmlutil.hh
new file mode 100644
index 0000000..ad96956
--- /dev/null
+++ b/src/htmlutil.hh
@@ -0,0 +1,21 @@
+#ifndef HTMLUTIL_HH
+#define HTMLUTIL_HH
+
+#include <string>
+#include <string_view>
+
+namespace html {
+
+enum class EscapeTarget {
+ BODY,
+ ATTRIBUTE,
+};
+
+std::string escape(std::string_view in,
+ EscapeTarget target = EscapeTarget::BODY);
+void escape(std::string_view in, std::string* out,
+ EscapeTarget target = EscapeTarget::BODY);
+
+} // namespace html
+
+#endif // HTMLUTIL_HH
diff --git a/src/http_protocol.cc b/src/http_protocol.cc
new file mode 100644
index 0000000..c0e40ad
--- /dev/null
+++ b/src/http_protocol.cc
@@ -0,0 +1,916 @@
+#include "common.hh"
+
+#include "http_protocol.hh"
+
+#include <memory>
+#include <string.h>
+#include <time.h>
+#include <vector>
+
+namespace {
+
+uint16_t number(std::string_view data, size_t start, size_t end) {
+ uint16_t ret = 0;
+ assert(start < end);
+ for (; start < end; ++start) {
+ ret *= 10;
+ ret += data[start] - '0';
+ }
+ return ret;
+}
+
+inline char lower_ascii(char c) {
+ return (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c;
+}
+
+inline bool is_lws(char c) {
+ return c == ' ' || c == '\t';
+}
+
+inline bool is_char(char c) {
+ return !(c & 0x80);
+}
+
+inline bool is_ctl(char c) {
+ return c < ' ' || c == 0x7f;
+}
+
+inline bool is_separator(char c) {
+ return is_lws(c) || c == '(' || c == ')' || c == '<' || c == '>' || c == '@'
+ || c == ',' || c == ';' || c == ':' || c == '\\' || c == '\"' || c == '/'
+ || c == '[' || c == ']' || c == '?' || c == '=' || c == '{' || c == '}';
+}
+
+inline bool is_token(char c) {
+ return is_char(c) && !is_ctl(c) && !is_separator(c);
+}
+
+void make_lowercase(std::string& data, size_t start, size_t end) {
+ for (size_t i = start; i <= end; ++i) {
+ char lower = lower_ascii(data[i]);
+ if (lower != data[i])
+ data[i] = lower;
+ }
+}
+
+bool allow_header_append(std::string_view name) {
+ // These headers doesn't handle being merged with ',' even if the standard
+ // say they must
+ return !(name == "set-cookie" || name == "set-cookie2");
+}
+
+enum ParseResult {
+ GOOD,
+ BAD,
+ INCOMPLETE,
+};
+
+class HeaderIteratorImpl : public HeaderIterator {
+public:
+ HeaderIteratorImpl(std::string_view data, std::vector<size_t> const* headers)
+ : data_(data), headers_(headers), iter_(headers_->begin()) {
+ }
+
+ bool valid() const override {
+ return iter_ != headers_->end();
+ }
+
+ std::string_view name() const override {
+ return data_.substr(iter_[0], iter_[1] - iter_[0]);
+ }
+
+ std::string value() const override {
+ std::string ret(data_.substr(iter_[2], iter_[3] - iter_[2]));
+ if (allow_header_append(name())) {
+ auto i = iter_ + 4;
+ while (i != headers_->end()) {
+ if (i[0] != i[1]) break;
+ ret.push_back(',');
+ ret.append(data_.substr(i[2], i[3] - i[2]));
+ i += 4;
+ }
+ }
+ return ret;
+ }
+
+ void next() override {
+ if (iter_ != headers_->end()) {
+ while (true) {
+ iter_ += 4;
+ if (iter_ == headers_->end() || iter_[0] != iter_[1])
+ break;
+ }
+ }
+ }
+
+private:
+ std::string_view const data_;
+ std::vector<size_t> const* const headers_;
+ std::vector<size_t>::const_iterator iter_;
+};
+
+class FilterHeaderIteratorImpl : public HeaderIteratorImpl {
+public:
+ FilterHeaderIteratorImpl(std::string_view data,
+ std::vector<size_t> const* headers,
+ std::string_view filter)
+ : HeaderIteratorImpl(data, headers), filter_(filter) {
+ check_filter();
+ }
+
+ void next() override {
+ HeaderIteratorImpl::next();
+ check_filter();
+ }
+
+private:
+ void check_filter() {
+ while (true) {
+ if (!valid() || name() == filter_)
+ return;
+ HeaderIteratorImpl::next();
+ }
+ }
+
+ std::string_view const filter_;
+};
+
+class HeaderTokenIteratorImpl : public HeaderTokenIterator {
+public:
+ explicit HeaderTokenIteratorImpl(std::unique_ptr<HeaderIterator>&& header)
+ : header_(std::move(header)), start_(0), middle_(0), end_(0) {
+ check_token();
+ }
+
+ bool valid() const override {
+ return header_->valid();
+ }
+
+ std::string token() const override {
+ return header_->value().substr(start_, middle_ - start_);
+ }
+
+ void next() override {
+ start_ = end_;
+ check_token();
+ }
+
+private:
+ static size_t skip_lws(std::string const& str, size_t pos) {
+ while (pos < str.size() && is_lws(str[pos])) ++pos;
+ return pos;
+ }
+
+ static size_t skip_token(std::string const& str, size_t pos) {
+ assert(is_token(str[pos]));
+ ++pos;
+ while (pos < str.size() && is_token(str[pos])) ++pos;
+ return pos;
+ }
+
+ static size_t skip_quoted(std::string const& str, size_t pos) {
+ assert(str[pos] == '"');
+ ++pos;
+ while (pos < str.size()) {
+ if (str[pos] == '\\') {
+ pos += 2;
+ } else if (str[pos] == '\"') {
+ ++pos;
+ break;
+ } else {
+ ++pos;
+ }
+ }
+ return pos;
+ }
+
+ void check_token() {
+ while (true) {
+ if (!header_->valid()) return;
+ auto const& value = header_->value();
+ start_ = skip_lws(value, start_);
+ if (start_ >= value.size()) {
+ header_->next();
+ start_ = 0;
+ continue;
+ }
+ if (!is_token(value[start_])) {
+ if (value[start_] != ';') {
+ ++start_;
+ while (start_ < value.size()
+ && !(is_lws(value[start_]) || value[start_] == ','
+ || value[start_] == ';')) {
+ ++start_;
+ }
+ if (start_ < value.size() && value[start_] != ';') {
+ continue;
+ }
+ }
+ // This will cause us to loop again after paramters
+ // are read
+ middle_ = start_;
+ } else {
+ middle_ = skip_token(value, start_);
+ }
+ end_ = middle_;
+ while (true) {
+ end_ = skip_lws(value, end_);
+ if (end_ == value.size() || value[end_] != ';') break;
+ end_ = skip_lws(value, end_ + 1);
+ if (!is_token(value[end_])) {
+ while (end_ < value.size() && !is_separator(value[end_])) ++end_;
+ continue;
+ }
+ end_ = skip_token(value, end_);
+ end_ = skip_lws(value, end_);
+ if (end_ == value.size() || value[end_] != '=') break;
+ end_ = skip_lws(value, end_ + 1);
+ if (end_ < value.size() && value[end_] == '"') {
+ end_ = skip_quoted(value, end_);
+ } else {
+ if (!is_token(value[end_])) {
+ while (end_ < value.size() && !is_separator(value[end_])) ++end_;
+ continue;
+ }
+ end_ = skip_token(value, end_);
+ }
+ }
+ if (end_ < value.size() && value[end_] == ',') ++end_;
+ if (start_ < middle_) return;
+ start_ = end_;
+ }
+ }
+
+ std::unique_ptr<HeaderIterator> header_;
+ size_t start_;
+ size_t middle_;
+ size_t end_;
+};
+
+size_t find_newline(std::string_view data, size_t start, size_t* next) {
+ assert(start <= data.size());
+ for (; start < data.size(); ++start) {
+ if (data[start] == '\r') {
+ if (start + 1 < data.size() && data[start + 1] == '\n') {
+ if (next) *next = start + 2;
+ } else {
+ if (next) *next = start + 1;
+ }
+ return start;
+ } else if (data[start] == '\n') {
+ if (next) *next = start + 1;
+ return start;
+ }
+ }
+ return std::string::npos;
+}
+
+size_t find(std::string_view data, size_t start, char c, size_t end) {
+ assert(start <= end);
+ for (; start < end; ++start) {
+ if (data[start] == c) return start;
+ }
+ return std::string::npos;
+}
+
+size_t skip_lws(std::string_view data, size_t start, size_t end) {
+ assert(start <= end);
+ while (start < end && is_lws(data[start])) ++start;
+ return start;
+}
+
+size_t valid_number(std::string_view data, size_t start, size_t end) {
+ assert(start <= end);
+ if (start == end)
+ return std::string::npos;
+ if (data[start] == '0') {
+ return start + 1;
+ }
+ if (data[start] < '0' || data[start] > '9')
+ return std::string::npos;
+ for (++start; start < end; ++start) {
+ if (data[start] < '0' || data[start] > '9')
+ break;
+ }
+ return start;
+}
+
+ParseResult parse_headers(std::string_view data, size_t* offset,
+ std::vector<size_t>* headers) {
+ assert(*offset <= data.size());
+ assert(headers->empty());
+ while (true) {
+ auto start = *offset;
+ auto end = find_newline(data, start, offset);
+ if (end == std::string::npos)
+ return INCOMPLETE;
+ if (end == start) {
+ // The final newline can only be a alone '\r' if the one in front of
+ // it is also '\r', otherwise we expect a missing '\n'
+ if (data[start - 1] == '\n' && data[*offset - 1] == '\r') {
+ return INCOMPLETE;
+ }
+ break;
+ }
+ if (is_lws(data[start])) {
+ if (headers->empty())
+ return BAD;
+ headers->push_back(start);
+ headers->push_back(start);
+ headers->push_back(start + 1);
+ headers->push_back(end);
+ } else {
+ auto colon = find(data, start, ':', end);
+ if (colon == std::string::npos) return BAD;
+ auto value_start = skip_lws(data, colon + 1, end);
+ while (colon > start && is_lws(data[colon - 1])) --colon;
+ headers->push_back(start);
+ headers->push_back(colon);
+ headers->push_back(value_start);
+ headers->push_back(end);
+ }
+ }
+ return GOOD;
+}
+
+std::string make_lowercase_header_names(std::string_view data,
+ std::vector<size_t> const& headers) {
+ std::string ret(data);
+ for (size_t i = 0; i < headers.size(); i += 4) {
+ make_lowercase(ret, headers[i], headers[i + 1]);
+ }
+ return ret;
+}
+
+class AbstractHttp : public virtual HttpPackage {
+public:
+ AbstractHttp(std::string data, bool good, size_t proto_start,
+ size_t proto_slash, size_t proto_dot, size_t proto_end,
+ std::vector<size_t> headers, size_t content_start)
+ : data_(std::move(data)), good_(good), proto_start_(proto_start),
+ proto_slash_(proto_slash), proto_dot_(proto_dot), proto_end_(proto_end),
+ headers_(std::move(headers)), content_start_(content_start) {
+ }
+
+ bool good() const override {
+ return good_;
+ }
+
+ std::string_view proto() const override {
+ return std::string_view(data_).substr(
+ proto_start_, proto_slash_ - proto_start_);
+ }
+
+ Version proto_version() const override {
+ Version ret;
+ ret.major = number(data_, proto_slash_ + 1, proto_dot_);
+ ret.minor = number(data_, proto_dot_ + 1, proto_end_);
+ return ret;
+ }
+
+ std::unique_ptr<HeaderIterator> header() const override {
+ return std::make_unique<HeaderIteratorImpl>(data_, &headers_);
+ }
+ std::unique_ptr<HeaderIterator> header(
+ std::string_view name) const override {
+ return std::make_unique<FilterHeaderIteratorImpl>(data_, &headers_, name);
+ }
+
+ std::unique_ptr<HeaderTokenIterator> header_tokens(std::string_view name)
+ const override {
+ return std::make_unique<HeaderTokenIteratorImpl>(header(name));
+ }
+
+ size_t size() const override {
+ return content_start_;
+ }
+
+protected:
+ std::string const data_;
+ bool const good_;
+ size_t const proto_start_;
+ size_t const proto_slash_;
+ size_t const proto_dot_;
+ size_t const proto_end_;
+ std::vector<size_t> const headers_;
+ size_t const content_start_;
+};
+
+class HttpResponseImpl : public HttpResponse, protected AbstractHttp {
+public:
+ HttpResponseImpl(std::string data, bool good,
+ size_t proto_start, size_t proto_slash,
+ size_t proto_dot, size_t proto_end,
+ size_t status_start,
+ size_t status_end, size_t status_msg_start,
+ size_t status_msg_end,
+ std::vector<size_t> headers,
+ size_t content_start)
+ : AbstractHttp(std::move(data), good, proto_start, proto_slash, proto_dot,
+ proto_end, std::move(headers), content_start),
+ status_start_(status_start), status_end_(status_end),
+ status_msg_start_(status_msg_start), status_msg_end_(status_msg_end) {
+ }
+
+ uint16_t status_code() const override {
+ return number(data_, status_start_, status_end_);
+ }
+
+ std::string_view status_message() const override {
+ return std::string_view(data_).substr(status_msg_start_,
+ status_msg_end_ - status_msg_start_);
+ }
+
+ static std::unique_ptr<HttpResponse> parse(std::string_view data) {
+ size_t content_start = 0;
+ size_t status_msg_end = find_newline(data, 0, &content_start);
+ if (status_msg_end == std::string::npos)
+ return nullptr;
+ size_t proto_start = 0;
+ size_t proto_slash = find(data, 0, '/', status_msg_end);
+ if (proto_slash == std::string::npos)
+ return make_bad_http_response();
+ size_t proto_dot = valid_number(data, proto_slash + 1, status_msg_end);
+ if (proto_dot == std::string::npos || data[proto_dot] != '.')
+ return make_bad_http_response();
+ size_t proto_end = valid_number(data, proto_dot + 1, status_msg_end);
+ if (proto_end == std::string::npos || !is_lws(data[proto_end]))
+ return make_bad_http_response();
+ size_t status_start = skip_lws(data, proto_end + 1, status_msg_end);
+ size_t status_end = valid_number(data, status_start, status_msg_end);
+ if (status_end == std::string::npos)
+ return make_bad_http_response();
+ size_t status_msg_start;
+ if (is_lws(data[status_end])) {
+ status_msg_start = skip_lws(data, status_end + 1, status_msg_end);
+ } else {
+ status_msg_start = status_end;
+ if (status_msg_start != status_msg_end)
+ return make_bad_http_response();
+ }
+
+ std::vector<size_t> headers;
+ switch (parse_headers(data, &content_start, &headers)) {
+ case GOOD:
+ return std::make_unique<HttpResponseImpl>(
+ make_lowercase_header_names(data, headers), true,
+ proto_start, proto_slash, proto_dot, proto_end,
+ status_start, status_end, status_msg_start, status_msg_end,
+ std::move(headers), content_start);
+ case BAD:
+ return make_bad_http_response();
+ case INCOMPLETE:
+ return nullptr;
+ }
+ assert(false);
+ return nullptr;
+ }
+
+private:
+ static std::unique_ptr<HttpResponse> make_bad_http_response() {
+ return std::make_unique<HttpResponseImpl>(std::string(), false,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ std::vector<size_t>(), 0);
+ }
+
+ size_t const status_start_;
+ size_t const status_end_;
+ size_t const status_msg_start_;
+ size_t const status_msg_end_;
+};
+
+class HttpRequestImpl : public HttpRequest, protected AbstractHttp {
+public:
+ HttpRequestImpl(std::string data, bool good,
+ size_t method_end, size_t url_start, size_t url_end,
+ size_t proto_start, size_t proto_slash,
+ size_t proto_dot, size_t proto_end,
+ std::vector<size_t> headers,
+ size_t content_start)
+ : AbstractHttp(std::move(data), good, proto_start, proto_slash,
+ proto_dot, proto_end, std::move(headers), content_start),
+ method_end_(method_end), url_start_(url_start), url_end_(url_end) {
+ }
+
+ std::string_view method() const override {
+ return std::string_view(data_).substr(0, method_end_);
+ }
+
+ std::string_view url() const override {
+ return std::string_view(data_).substr(url_start_, url_end_ - url_start_);
+ }
+
+ static std::unique_ptr<HttpRequest> parse(std::string_view data) {
+ size_t content_start = 0;
+ size_t proto_end = find_newline(data, 0, &content_start);
+ if (proto_end == std::string::npos)
+ return nullptr;
+ size_t method_end = 0;
+ while (method_end < proto_end && !is_lws(data[method_end])) {
+ ++method_end;
+ }
+ if (method_end == 0 || method_end == proto_end)
+ return make_bad_request();
+ size_t url_start = skip_lws(data, method_end + 1, proto_end);
+ size_t url_end = url_start;
+ while (url_end < proto_end && !is_lws(data[url_end]))
+ ++url_end;
+ if (url_end == url_start || url_end == proto_end)
+ return make_bad_request();
+ size_t proto_start = skip_lws(data, url_end + 1, proto_end);
+ size_t proto_slash = find(data, proto_start, '/', proto_end);
+ if (proto_slash == std::string::npos)
+ return make_bad_request();
+ size_t proto_dot = valid_number(data, proto_slash + 1, proto_end);
+ if (proto_dot == std::string::npos || data[proto_dot] != '.')
+ return make_bad_request();
+ auto tmp = valid_number(data, proto_dot + 1, proto_end);
+ if (tmp != proto_end)
+ return make_bad_request();
+
+ std::vector<size_t> headers;
+ switch (parse_headers(data, &content_start, &headers)) {
+ case GOOD:
+ return std::make_unique<HttpRequestImpl>(
+ make_lowercase_header_names(data, headers), true,
+ method_end, url_start, url_end,
+ proto_start, proto_slash,
+ proto_dot, proto_end,
+ std::move(headers), content_start);
+ case BAD:
+ return make_bad_request();
+ case INCOMPLETE:
+ return nullptr;
+ }
+ assert(false);
+ return nullptr;
+ }
+
+private:
+ static std::unique_ptr<HttpRequest> make_bad_request() {
+ return std::make_unique<HttpRequestImpl>("", false, 0, 0, 0, 0, 0, 0, 0,
+ std::vector<size_t>(), 0);
+ }
+
+ size_t const method_end_;
+ size_t const url_start_;
+ size_t const url_end_;
+};
+
+class AbstractHttpBuilder {
+public:
+ void add_header(std::string name, std::string value) {
+ headers_.emplace_back(std::move(name), std::move(value));
+ }
+
+ bool build(Buffer* dst) const {
+ for (auto const& pair : headers_) {
+ if (pair.first.empty()) {
+ if (!append(dst, " "))
+ return false;
+ } else {
+ if (!append(dst, pair.first) ||
+ !append(dst, ": "))
+ return false;
+ }
+ if (!append(dst, pair.second) ||
+ !append(dst, "\r\n"))
+ return false;
+ }
+ return append(dst, "\r\n");
+ }
+
+ size_t size() const {
+ size_t ret = 0;
+ for (auto const& pair : headers_) {
+ if (pair.first.empty()) {
+ ++ret; // ' '
+ } else {
+ ret += pair.first.size() + 2; // ": "
+ }
+ ret += pair.second.size() + 2; // \r\”
+ }
+ return ret + 2; // \r\n
+ }
+
+protected:
+ bool append(Buffer* dst, std::string_view str) const {
+ return Buffer::write(dst, str.data(), str.size()) == str.size();
+ }
+
+ std::vector<std::pair<std::string, std::string>> headers_;
+};
+
+class HttpRequestBuilderImpl : public HttpRequestBuilder, AbstractHttpBuilder {
+public:
+ HttpRequestBuilderImpl(std::string method, std::string url, std::string proto,
+ Version version)
+ : method_(std::move(method)), url_(std::move(url)),
+ proto_(std::move(proto)), version_(version) {}
+
+ void add_header(std::string name, std::string value) override {
+ AbstractHttpBuilder::add_header(std::move(name), std::move(value));
+ }
+
+ bool build(Buffer* dst) const override {
+ if (!append(dst, method_) ||
+ !append(dst, " ") ||
+ !append(dst, url_) ||
+ !append(dst, " ") ||
+ !append(dst, proto_) ||
+ !append(dst, "/"))
+ return false;
+ char tmp[10];
+ auto len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(version_.major));
+ if (!append(dst, std::string_view(tmp, len)))
+ return false;
+ if (!append(dst, "."))
+ return false;
+ len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(version_.minor));
+ if (!append(dst, std::string_view(tmp, len)))
+ return false;
+ if (!append(dst, "\r\n"))
+ return false;
+ return AbstractHttpBuilder::build(dst);
+ }
+
+ size_t size() const override {
+ size_t ret = 0;
+ ret += method_.size() + 1 + url_.size() + 1 + proto_.size() + 1;
+ char tmp[10];
+ auto len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(version_.major));
+ ret += len;
+ ++ret; // '.'
+ len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(version_.minor));
+ ret += len;
+ ret += 2; // \r\n
+ return ret + AbstractHttpBuilder::size();
+ }
+
+private:
+ std::string const method_;
+ std::string const url_;
+ std::string const proto_;
+ Version const version_;
+};
+
+class HttpResponseBuilderImpl : public HttpResponseBuilder,
+ AbstractHttpBuilder {
+public:
+ HttpResponseBuilderImpl(std::string proto, Version version,
+ uint16_t status_code, std::string status)
+ : proto_(std::move(proto)), version_(version), status_code_(status_code),
+ status_(std::move(status)) {
+ }
+
+ void add_header(std::string name, std::string value) override {
+ AbstractHttpBuilder::add_header(std::move(name), std::move(value));
+ }
+
+ bool build(Buffer* dst) const override {
+ if (!append(dst, proto_) ||
+ !append(dst, "/"))
+ return false;
+ char tmp[10];
+ auto len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(version_.major));
+ if (!append(dst, std::string_view(tmp, len)) ||
+ !append(dst, "."))
+ return false;
+ len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(version_.minor));
+ if (!append(dst, std::string_view(tmp, len)) ||
+ !append(dst, " "))
+ return false;
+ len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(status_code_));
+ if (!append(dst, std::string_view(tmp, len)) ||
+ !append(dst, " ") ||
+ !append(dst, status_) ||
+ !append(dst, "\r\n"))
+ return false;
+ return AbstractHttpBuilder::build(dst);
+ }
+
+ size_t size() const override {
+ size_t ret = 0;
+ ret += proto_.size();
+ ++ret; // "/"
+ char tmp[10];
+ auto len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(version_.major));
+ ret += len;
+ ++ret; // "."
+ len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(version_.minor));
+ ret += len;
+ ++ret; // " "
+ len = snprintf(tmp, sizeof(tmp), "%u",
+ static_cast<unsigned int>(status_code_));
+ ret += len;
+ ++ret; // " "
+ ret += status_.length();
+ ret += 2; // \r\n
+ return ret + AbstractHttpBuilder::size();
+ }
+
+private:
+ std::string const proto_;
+ Version const version_;
+ uint16_t const status_code_;
+ std::string const status_;
+};
+
+class CgiResponseBuilderImpl : public CgiResponseBuilder,
+ AbstractHttpBuilder {
+public:
+ explicit CgiResponseBuilderImpl(uint16_t status_code) {
+ AbstractHttpBuilder::add_header("Status", std::to_string(status_code));
+ }
+
+ void add_header(std::string name, std::string value) override {
+ AbstractHttpBuilder::add_header(std::move(name), std::move(value));
+ }
+
+ bool build(Buffer* dst) const override {
+ return AbstractHttpBuilder::build(dst);
+ }
+
+ size_t size() const override {
+ return AbstractHttpBuilder::size();
+ }
+};
+
+} // namespace
+
+// static
+std::unique_ptr<HttpResponse> HttpResponse::parse(RoBuffer* buffer) {
+ size_t want = 1024;
+ size_t last_avail = 0;
+ while (true) {
+ size_t avail;
+ auto* rptr = buffer->rbuf(want, avail);
+ if (avail == last_avail)
+ return nullptr;
+ last_avail = avail;
+ auto resp = HttpResponseImpl::parse(std::string_view(rptr, avail));
+ if (resp) {
+ buffer->rcommit(resp->size());
+ return resp;
+ }
+ want = avail + 1024;
+ }
+}
+
+std::string HttpPackage::first_header(std::string_view name) const {
+ static std::string empty_str;
+ auto iter = header(name);
+ if (iter->valid()) {
+ return iter->value();
+ }
+ return empty_str;
+}
+
+// static
+std::unique_ptr<HttpRequest> HttpRequest::parse(RoBuffer* buffer) {
+ size_t want = 1024;
+ size_t last_avail = 0;
+ while (true) {
+ size_t avail;
+ auto* rptr = buffer->rbuf(want, avail);
+ if (avail == last_avail)
+ return nullptr;
+ last_avail = avail;
+ auto req = HttpRequestImpl::parse(std::string_view(rptr, avail));
+ if (req) {
+ buffer->rcommit(req->size());
+ return req;
+ }
+ want = avail + 1024;
+ }
+}
+
+// static
+std::unique_ptr<HttpRequestBuilder> HttpRequestBuilder::create(
+ std::string method,
+ std::string url,
+ std::string proto,
+ Version version) {
+ return std::make_unique<HttpRequestBuilderImpl>(std::move(method),
+ std::move(url),
+ std::move(proto), version);
+}
+
+// static
+std::unique_ptr<HttpResponseBuilder> HttpResponseBuilder::create(
+ std::string proto,
+ Version version,
+ uint16_t status_code,
+ std::string status) {
+ return std::make_unique<HttpResponseBuilderImpl>(std::move(proto), version,
+ status_code,
+ std::move(status));
+}
+
+// static
+std::unique_ptr<CgiResponseBuilder> CgiResponseBuilder::create(
+ uint16_t status_code) {
+ return std::make_unique<CgiResponseBuilderImpl>(status_code);
+}
+
+std::string_view http_standard_message(uint16_t code) {
+ switch (code) {
+ case 100:
+ return "Continue";
+ case 101:
+ return "Switching Protocols";
+ case 200:
+ return "OK";
+ case 201:
+ return "Created";
+ case 202:
+ return "Accepted";
+ case 203:
+ return "Non-Authorative Information";
+ case 204:
+ return "No Content";
+ case 205:
+ return "Reset Content";
+ case 206:
+ return "Partial Content";
+ case 300:
+ return "Multiple Choices";
+ case 301:
+ return "Moved Permanently";
+ case 302:
+ return "Found";
+ case 303:
+ return "See Other";
+ case 304:
+ return "Not Modified";
+ case 305:
+ return "Use Proxy";
+ case 307:
+ return "Temporary Redirect";
+ case 400:
+ return "Bad Request";
+ case 401:
+ return "Unauthorized";
+ case 402:
+ return "Payment Required";
+ case 403:
+ return "Forbidden";
+ case 404:
+ return "Not Found";
+ case 405:
+ return "Method Not Allowed";
+ case 406:
+ return "Not Acceptable";
+ case 407:
+ return "Proxy Authentication Required";
+ case 408:
+ return "Request Timeout";
+ case 409:
+ return "Conflict";
+ case 410:
+ return "Gone";
+ case 411:
+ return "Length Required";
+ case 412:
+ return "Precondition Failed";
+ case 413:
+ return "Request Entity Too Large";
+ case 414:
+ return "Request-URI Too Long";
+ case 415:
+ return "Unsupported Media Type";
+ case 416:
+ return "Requested Range Not Satisfiable";
+ case 417:
+ return "Expectation Failed";
+ case 500:
+ return "Internal Server Error";
+ case 501:
+ return "Not Implemented";
+ case 502:
+ return "Bad Gateway";
+ case 503:
+ return "Service Unavailable";
+ case 504:
+ return "Gateway Timeout";
+ case 505:
+ return "HTTP Version Not Supported";
+ }
+ return "";
+}
+
+std::string http_date(time_t in) {
+ char tmp[50];
+ auto len = strftime(tmp, sizeof(tmp), "%a, %d %b %Y %H:%M:%S GMT",
+ gmtime(&in));
+ return std::string(tmp, len);
+}
diff --git a/src/http_protocol.hh b/src/http_protocol.hh
new file mode 100644
index 0000000..e558b27
--- /dev/null
+++ b/src/http_protocol.hh
@@ -0,0 +1,170 @@
+#ifndef HTTP_PROTOCOL_HH
+#define HTTP_PROTOCOL_HH
+
+#include <memory>
+#include <stdint.h>
+#include <string>
+#include <string_view>
+
+#include "buffer.hh"
+
+// glibc defines these even tho it shouldn't if you ask posix
+#ifdef major
+# undef major
+#endif
+#ifdef minor
+# undef minor
+#endif
+
+struct Version {
+ uint16_t major;
+ uint16_t minor;
+
+ Version()
+ : major(0), minor(0) {
+ }
+ Version(uint16_t major, uint16_t minor)
+ : major(major), minor(minor) {
+ }
+};
+
+class HeaderIterator {
+public:
+ virtual ~HeaderIterator() = default;
+
+ virtual bool valid() const = 0;
+ virtual std::string_view name() const = 0;
+ virtual std::string value() const = 0;
+ virtual void next() = 0;
+
+protected:
+ HeaderIterator() = default;
+ HeaderIterator(HeaderIterator const&) = delete;
+ HeaderIterator& operator=(HeaderIterator const&) = delete;
+};
+
+class HeaderTokenIterator {
+public:
+ virtual ~HeaderTokenIterator() = default;
+
+ virtual bool valid() const = 0;
+ virtual std::string token() const = 0;
+ virtual void next() = 0;
+
+protected:
+ HeaderTokenIterator() = default;
+ HeaderTokenIterator(HeaderTokenIterator const&) = delete;
+ HeaderTokenIterator& operator=(HeaderTokenIterator const&) = delete;
+};
+
+class HttpPackage {
+public:
+ virtual ~HttpPackage() = default;
+
+ virtual bool good() const = 0;
+
+ virtual std::string_view proto() const = 0;
+ virtual Version proto_version() const = 0;
+ virtual std::unique_ptr<HeaderIterator> header() const = 0;
+ virtual std::unique_ptr<HeaderIterator> header(
+ std::string_view name) const = 0;
+ std::string first_header(std::string_view name) const;
+ virtual std::unique_ptr<HeaderTokenIterator> header_tokens(
+ std::string_view name) const = 0;
+ virtual size_t size() const = 0;
+
+protected:
+ HttpPackage() = default;
+ HttpPackage(HttpPackage const&) = delete;
+ HttpPackage& operator=(HttpPackage const&) = delete;
+};
+
+class HttpResponse : public virtual HttpPackage {
+public:
+ virtual ~HttpResponse() = default;
+
+ static std::unique_ptr<HttpResponse> parse(RoBuffer* buffer);
+
+ virtual uint16_t status_code() const = 0;
+ virtual std::string_view status_message() const = 0;
+
+protected:
+ HttpResponse() = default;
+ HttpResponse(HttpResponse const&) = delete;
+ HttpResponse& operator=(HttpResponse const&) = delete;
+};
+
+class HttpRequest : public virtual HttpPackage {
+public:
+ virtual ~HttpRequest() = default;
+
+ static std::unique_ptr<HttpRequest> parse(RoBuffer* buffer);
+
+ virtual std::string_view method() const = 0;
+ virtual std::string_view url() const = 0;
+
+protected:
+ HttpRequest() = default;
+ HttpRequest(HttpRequest const&) = delete;
+ HttpRequest& operator=(HttpRequest const&) = delete;
+};
+
+class HttpResponseBuilder {
+public:
+ virtual ~HttpResponseBuilder() = default;
+
+ static std::unique_ptr<HttpResponseBuilder> create(
+ std::string proto, Version version,
+ uint16_t status_code, std::string status_message);
+
+ virtual void add_header(std::string name, std::string value) = 0;
+
+ virtual bool build(Buffer* dst) const = 0;
+ virtual size_t size() const = 0;
+
+protected:
+ HttpResponseBuilder() = default;
+ HttpResponseBuilder(HttpResponseBuilder const&) = delete;
+ HttpResponseBuilder& operator=(HttpResponseBuilder const&) = delete;
+};
+
+class HttpRequestBuilder {
+public:
+ virtual ~HttpRequestBuilder() = default;
+
+ static std::unique_ptr<HttpRequestBuilder> create(
+ std::string method, std::string url, std::string proto, Version version);
+
+ virtual void add_header(std::string name, std::string value) = 0;
+
+ virtual bool build(Buffer* dst) const = 0;
+ virtual size_t size() const = 0;
+
+protected:
+ HttpRequestBuilder() = default;
+ HttpRequestBuilder(HttpRequestBuilder const&) = delete;
+ HttpRequestBuilder& operator=(HttpRequestBuilder const&) = delete;
+};
+
+class CgiResponseBuilder {
+public:
+ virtual ~CgiResponseBuilder() = default;
+
+ static std::unique_ptr<CgiResponseBuilder> create(uint16_t status_code);
+
+ virtual void add_header(std::string name, std::string value) = 0;
+
+ virtual bool build(Buffer* dst) const = 0;
+ virtual size_t size() const = 0;
+
+protected:
+ CgiResponseBuilder() = default;
+ CgiResponseBuilder(CgiResponseBuilder const&) = delete;
+ CgiResponseBuilder& operator=(CgiResponseBuilder const&) = delete;
+};
+
+std::string_view http_standard_message(uint16_t code);
+
+std::string http_date(time_t in);
+
+#endif // HTTP_PROTOCOL_HH
diff --git a/src/image.cc b/src/image.cc
new file mode 100644
index 0000000..111568c
--- /dev/null
+++ b/src/image.cc
@@ -0,0 +1,344 @@
+#include "common.hh"
+
+#include "buffer.hh"
+#include "image.hh"
+#include "mime_types.hh"
+
+#include <algorithm>
+#include <limits>
+
+#if HAVE_JPEG
+#include <jpeglib.h>
+#include <setjmp.h>
+#endif
+
+#if HAVE_EXIF
+#include <libexif/exif-data.h>
+#include <libexif/exif-entry.h>
+#include <libexif/exif-format.h>
+#include <libexif/exif-ifd.h>
+#include <libexif/exif-loader.h>
+#endif
+
+namespace {
+
+bool is_jpeg(const unsigned char* data, size_t size);
+
+class ThumbnailImpl : public Image::Thumbnail {
+public:
+ ThumbnailImpl(std::string mime_type, uint64_t size)
+ : mime_type_(std::move(mime_type)), size_(size) {}
+
+ std::string_view mime_type() const override {
+ return mime_type_;
+ }
+
+ uint64_t size() const override {
+ return size_;
+ }
+
+private:
+ std::string mime_type_;
+ uint64_t size_;
+};
+
+class ImageImpl : public Image {
+public:
+ ImageImpl(uint64_t width, uint64_t height)
+ : width_(width), height_(height) {}
+
+ uint64_t width() const override {
+ return width_;
+ }
+
+ uint64_t height() const override {
+ return height_;
+ }
+
+ Location location() const override {
+ return location_;
+ }
+
+ Rotation rotation() const override {
+ return rotation_;
+ }
+
+ Date date() const override {
+ return date_;
+ }
+
+ Thumbnail* thumbnail() const override {
+ return thumbnail_.get();
+ }
+
+ void set_location(Location location) {
+ location_ = location;
+ }
+
+ void set_rotation(Rotation rotation) {
+ rotation_ = rotation;
+ }
+
+ void set_date(Date date) {
+ date_ = date;
+ }
+
+ void set_thumbnail(std::string mime_type, uint64_t size) {
+ thumbnail_ = std::make_unique<ThumbnailImpl>(std::move(mime_type), size);
+ }
+
+private:
+ uint64_t width_;
+ uint64_t height_;
+ Location location_;
+ Rotation rotation_{Rotation::UNKNOWN};
+ Date date_;
+ std::unique_ptr<ThumbnailImpl> thumbnail_;
+};
+
+#if HAVE_EXIF
+double make_double(ExifRational rat) {
+ return static_cast<double>(rat.numerator) / rat.denominator;
+}
+
+void load_exif(std::filesystem::path const& path, ImageImpl* img) {
+ ExifData *data = exif_data_new_from_file(path.c_str());
+ if (!data)
+ return;
+
+ auto byte_order = exif_data_get_byte_order(data);
+
+ auto* entry = exif_content_get_entry(data->ifd[EXIF_IFD_0],
+ EXIF_TAG_ORIENTATION);
+ if (entry && entry->format == EXIF_FORMAT_SHORT && entry->components == 1) {
+ auto orientation = exif_get_short(entry->data, byte_order);
+ switch (orientation) {
+ case 1:
+ img->set_rotation(Rotation::NONE);
+ break;
+ case 2:
+ img->set_rotation(Rotation::MIRRORED);
+ break;
+ case 3:
+ img->set_rotation(Rotation::ROTATED_180);
+ break;
+ case 4:
+ img->set_rotation(Rotation::ROTATED_180_MIRRORED);
+ break;
+ case 5:
+ img->set_rotation(Rotation::ROTATED_90);
+ break;
+ case 6:
+ img->set_rotation(Rotation::ROTATED_90_MIRRORED);
+ break;
+ case 7:
+ img->set_rotation(Rotation::ROTATED_270);
+ break;
+ case 8:
+ img->set_rotation(Rotation::ROTATED_270_MIRRORED);
+ break;
+ default:
+ break;
+ }
+ }
+ entry = exif_content_get_entry(data->ifd[EXIF_IFD_0], EXIF_TAG_DATE_TIME);
+ if (entry && entry->format == EXIF_FORMAT_ASCII) {
+ auto date = Date::from_format("%Y:%m:%d %H:%M:%S",
+ reinterpret_cast<char const*>(entry->data));
+ if (!date.empty())
+ img->set_date(date);
+ }
+ auto* lat = exif_content_get_entry(
+ data->ifd[EXIF_IFD_GPS],
+ static_cast<ExifTag>(EXIF_TAG_GPS_LATITUDE));
+ auto* lat_ref = exif_content_get_entry(
+ data->ifd[EXIF_IFD_GPS],
+ static_cast<ExifTag>(EXIF_TAG_GPS_LATITUDE_REF));
+ auto* lng = exif_content_get_entry(
+ data->ifd[EXIF_IFD_GPS],
+ static_cast<ExifTag>(EXIF_TAG_GPS_LONGITUDE));
+ auto* lng_ref = exif_content_get_entry(
+ data->ifd[EXIF_IFD_GPS],
+ static_cast<ExifTag>(EXIF_TAG_GPS_LONGITUDE_REF));
+ if (lat && lat->format == EXIF_FORMAT_RATIONAL && lat->components == 3 &&
+ lat_ref && lat_ref->format == EXIF_FORMAT_ASCII &&
+ lng && lng->format == EXIF_FORMAT_RATIONAL && lng->components == 3 &&
+ lng_ref && lng_ref->format == EXIF_FORMAT_ASCII) {
+ auto step = exif_format_get_size(EXIF_FORMAT_RATIONAL);
+ auto lat_double =
+ make_double(exif_get_rational(lat->data + step * 0, byte_order)) +
+ make_double(exif_get_rational(lat->data + step * 1,
+ byte_order)) / 60.0 +
+ make_double(exif_get_rational(lat->data + step * 2,
+ byte_order)) / 3600.0;
+ if (lat_ref->data[0] != 'N')
+ lat_double = -lat_double;
+ auto lng_double =
+ make_double(exif_get_rational(lng->data + step * 0, byte_order)) +
+ make_double(exif_get_rational(lng->data + step * 1,
+ byte_order)) / 60.0 +
+ make_double(exif_get_rational(lng->data + step * 2,
+ byte_order)) / 3600.0;
+ if (lng_ref->data[0] != 'E')
+ lng_double = -lng_double;
+
+ img->set_location(Location(lat_double, lng_double));
+ }
+
+ if (data->data) {
+ if (is_jpeg(data->data, data->size)) {
+ img->set_thumbnail("image/jpeg", data->size);
+ }
+ }
+
+ exif_data_free(data);
+}
+
+class ExifThumbnailReader : public ThumbnailReader {
+public:
+ ExifThumbnailReader()
+ : loader_(exif_loader_new()), data_(nullptr) {}
+
+ ~ExifThumbnailReader() {
+ if (loader_)
+ exif_loader_unref(loader_);
+ if (data_)
+ exif_data_free(data_);
+ }
+
+ Return drain(RoBuffer* buf, size_t* bytes) override {
+ if (bytes) *bytes = 0;
+ if (loader_) {
+ while (true) {
+ size_t avail;
+ auto* ptr = buf->rbuf(1, avail);
+ if (avail == 0)
+ return Return::NEED_MORE;
+ avail = std::min(
+ avail,
+ static_cast<size_t>(std::numeric_limits<unsigned int>::max()));
+ if (exif_loader_write(loader_, reinterpret_cast<unsigned char*>(
+ const_cast<char*>(ptr)), avail)) {
+ buf->rcommit(avail);
+ if (bytes) *bytes += avail;
+ // Loop to see if there is any more data avail.
+ } else {
+ // No data read or no hope there will be any.
+ break;
+ }
+ }
+ data_ = exif_loader_get_data(loader_);
+ exif_loader_unref(loader_);
+ loader_ = nullptr;
+ }
+ if (data_ && data_->data)
+ return Return::DONE;
+ return Return::ERR;
+ }
+
+ std::string_view data() const override {
+ return data_ && data_->data
+ ? std::string_view(reinterpret_cast<char const*>(data_->data),
+ data_->size)
+ : std::string_view();
+ }
+
+private:
+ ExifLoader* loader_;
+ ExifData* data_;
+};
+#else // HAVE_EXIF
+class FailingThumbnailReader : public ThumbnailReader {
+public:
+ FailingThumbnailReader() = default;
+
+ Return drain(RoBuffer*, size_t* bytes) override {
+ if (bytes) *bytes = 0;
+ return Return::ERR;
+ }
+
+ std::string_view data() const override {
+ return std::string_view();
+ }
+};
+#endif // HAVE_EXIT
+
+#if HAVE_JPEG
+struct my_jpeg_error {
+ struct jpeg_error_mgr base;
+ jmp_buf jmp_buffer;
+};
+
+void jpeg_error_exit(j_common_ptr cinfo) {
+ auto myerr = reinterpret_cast<my_jpeg_error*>(cinfo->err);
+ longjmp(myerr->jmp_buffer, 1);
+}
+
+bool is_jpeg(const unsigned char* data, size_t size) {
+ struct jpeg_decompress_struct info;
+ struct my_jpeg_error err;
+ info.err = jpeg_std_error(&err.base);
+ err.base.error_exit = jpeg_error_exit;
+ if (setjmp(err.jmp_buffer)) {
+ jpeg_destroy_decompress(&info);
+ return false;
+ }
+ jpeg_create_decompress(&info);
+ jpeg_mem_src(&info, data, size);
+ jpeg_read_header(&info, TRUE);
+ jpeg_destroy_decompress(&info);
+ return true;
+}
+
+std::unique_ptr<Image> load_jpeg(std::filesystem::path const& path) {
+ struct jpeg_decompress_struct info;
+ struct my_jpeg_error err;
+ FILE* fh = fopen(path.c_str(), "rb");
+ if (!fh)
+ return nullptr;
+ info.err = jpeg_std_error(&err.base);
+ err.base.error_exit = jpeg_error_exit;
+ if (setjmp(err.jmp_buffer)) {
+ jpeg_destroy_decompress(&info);
+ fclose(fh);
+ return nullptr;
+ }
+ jpeg_create_decompress(&info);
+ jpeg_stdio_src(&info, fh);
+ jpeg_read_header(&info, TRUE);
+
+ auto img = std::make_unique<ImageImpl>(info.image_width, info.image_height);
+
+ jpeg_destroy_decompress(&info);
+ fclose(fh);
+
+#if HAVE_EXIF
+ load_exif(path, img.get());
+#endif
+
+ return img;
+}
+#endif // HAVE_JPEG
+
+} // namespace
+
+std::unique_ptr<Image> Image::load(std::filesystem::path const& path) {
+ if (!path.has_extension())
+ return nullptr;
+ auto mime_type = mime_types::from_extension(
+ std::string(path.extension()).substr(1));
+#if HAVE_JPEG
+ if (mime_type == "image/jpeg") {
+ return load_jpeg(path);
+ }
+#endif // HAVE_JPEG
+ return nullptr;
+}
+
+std::unique_ptr<ThumbnailReader> ThumbnailReader::create() {
+#if HAVE_EXIF
+ return std::make_unique<ExifThumbnailReader>();
+#else
+ return std::make_unique<FailingThumbnailReader>();
+#endif
+}
diff --git a/src/image.hh b/src/image.hh
new file mode 100644
index 0000000..b9644b7
--- /dev/null
+++ b/src/image.hh
@@ -0,0 +1,67 @@
+#ifndef IMAGE_HH
+#define IMAGE_HH
+
+#include "date.hh"
+#include "location.hh"
+#include "rotation.hh"
+
+#include <filesystem>
+#include <memory>
+
+class RoBuffer;
+
+class Image {
+public:
+ virtual ~Image() = default;
+
+ class Thumbnail {
+ public:
+ virtual ~Thumbnail() = default;
+
+ virtual std::string_view mime_type() const = 0;
+ virtual uint64_t size() const = 0;
+
+ protected:
+ Thumbnail() = default;
+ };
+
+ static std::unique_ptr<Image> load(std::filesystem::path const& path);
+
+ virtual uint64_t width() const = 0;
+ virtual uint64_t height() const = 0;
+ virtual Location location() const = 0;
+ virtual Rotation rotation() const = 0;
+ virtual Date date() const = 0;
+
+ virtual Thumbnail* thumbnail() const = 0;
+
+protected:
+ Image() = default;
+ Image(Image const&) = delete;
+ Image& operator=(Image const&) = delete;
+};
+
+class ThumbnailReader {
+public:
+ virtual ~ThumbnailReader() = default;
+
+ static std::unique_ptr<ThumbnailReader> create();
+
+ enum class Return {
+ NEED_MORE, // Call drain() again if you want data.
+ DONE, // Data is available, no need to call drain() again.
+ ERR, // Error, no data will ever be available.
+ };
+
+ virtual Return drain(RoBuffer* in, size_t* bytes = nullptr) = 0;
+
+ // Returns empty until drain() returns Return::DONE.
+ virtual std::string_view data() const = 0;
+
+protected:
+ ThumbnailReader() = default;
+ ThumbnailReader(ThumbnailReader const&) = delete;
+ ThumbnailReader& operator=(ThumbnailReader const&) = delete;
+};
+
+#endif // IMAGE_HH
diff --git a/src/inet.cc b/src/inet.cc
new file mode 100644
index 0000000..1700a14
--- /dev/null
+++ b/src/inet.cc
@@ -0,0 +1,112 @@
+#include "common.hh"
+
+#include "inet.hh"
+#include "io.hh"
+#include "logger.hh"
+
+#include <algorithm>
+#include <errno.h>
+#include <netdb.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+
+namespace inet {
+
+unique_fd accept(Logger* logger, int fd, bool make_nonblock) {
+#if HAVE_ACCEPT4
+ unique_fd ret(accept4(fd, nullptr, nullptr,
+ make_nonblock ? SOCK_NONBLOCK : 0));
+ if (!ret)
+ logger->warn("accept: %s", strerror(errno));
+ return ret;
+#else
+ unique_fd ret(::accept(fd, nullptr, nullptr));
+ if (ret) {
+ if (make_nonblock && !io::make_nonblocking(ret.get())) {
+ logger->warn("make nonblock failed: %s", strerror(errno));
+ ret.reset();
+ }
+ } else {
+ logger->warn("accept: %s", strerror(errno));
+ }
+ return ret;
+#endif
+}
+
+bool bind_and_listen(Logger* logger,
+ std::string const& addr,
+ std::string const& port,
+ std::vector<unique_fd>* out) {
+ out->clear();
+ struct addrinfo hints = {};
+ struct addrinfo* ret;
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG | AI_PASSIVE;
+ auto err = getaddrinfo(addr.empty() ? nullptr : addr.c_str(),
+ port.c_str(), &hints, &ret);
+ if (err) {
+ logger->warn("%s:%s: getaddrinfo: %s",
+ addr.c_str(), port.c_str(), gai_strerror(err));
+ return false;
+ }
+
+ for (auto* rp = ret; rp != nullptr; rp = rp->ai_next) {
+ unique_fd fd(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol));
+ if (!fd)
+ continue;
+
+ if (bind(fd.get(), rp->ai_addr, rp->ai_addrlen)) {
+ logger->warn("%s:%s: bind: %s",
+ addr.c_str(), port.c_str(), strerror(errno));
+ continue;
+ }
+
+ if (listen(fd.get(), 4096)) {
+ logger->warn("%s:%s: listen: %s",
+ addr.c_str(), port.c_str(), strerror(errno));
+ continue;
+ }
+
+ out->emplace_back(std::move(fd));
+ }
+ freeaddrinfo(ret);
+ return !out->empty();
+}
+
+} // namespace inet
+
+namespace unix {
+
+unique_fd bind_and_listen(Logger* logger,
+ std::string_view path) {
+ unique_fd fd(socket(AF_UNIX, SOCK_STREAM, 0));
+ if (fd) {
+ sockaddr_un addr;
+ if (path.size() >= sizeof(addr.sun_path)) {
+ logger->warn("%.*s: Too long path",
+ static_cast<int>(path.size()), path.data());
+ return unique_fd();
+ }
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ std::copy_n(path.data(), path.size(), addr.sun_path);
+ addr.sun_path[path.size()] = '\0';
+ if (bind(fd.get(), reinterpret_cast<struct sockaddr*>(&addr),
+ sizeof(addr))) {
+ logger->warn("%.*s: bind: %s",
+ static_cast<int>(path.size()), path.data(), strerror(errno));
+ return unique_fd();
+ }
+ if (listen(fd.get(), 4096)) {
+ logger->warn("%.*s: listen: %s",
+ static_cast<int>(path.size()), path.data(), strerror(errno));
+ return unique_fd();
+ }
+ }
+ return fd;
+}
+
+} // namespace unix
diff --git a/src/inet.hh b/src/inet.hh
new file mode 100644
index 0000000..1575a17
--- /dev/null
+++ b/src/inet.hh
@@ -0,0 +1,29 @@
+#ifndef INET_HH
+#define INET_HH
+
+#include "unique_fd.hh"
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+class Logger;
+
+namespace inet {
+
+unique_fd accept(Logger* logger, int fd, bool make_nonblock = false);
+
+bool bind_and_listen(Logger* logger,
+ std::string const& addr,
+ std::string const& port,
+ std::vector<unique_fd>* out);
+
+} // namespace inet
+
+namespace unix {
+
+unique_fd bind_and_listen(Logger* logger, std::string_view path);
+
+} // namespace unix
+
+#endif // INET_HH
diff --git a/src/io.cc b/src/io.cc
new file mode 100644
index 0000000..04e6dfa
--- /dev/null
+++ b/src/io.cc
@@ -0,0 +1,234 @@
+#include "common.hh"
+
+#include "buffer.hh"
+#include "io.hh"
+#include "unique_fd.hh"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+namespace {
+
+bool mkdirs_internal(int root, std::filesystem::path const& path, mode_t mode,
+ unique_fd* out) {
+ if (mkdirat(root, path.c_str(), mode) == 0) {
+ if (out)
+ *out = io::openat(root, path, io::open_flags::path |
+ io::open_flags::directory);
+ return true;
+ }
+ if (errno == EEXIST) {
+ struct stat buf;
+ if (fstatat(root, path.c_str(), &buf, 0))
+ return false;
+ if (!S_ISDIR(buf.st_mode))
+ return false;
+ if (out)
+ *out = io::openat(root, path,
+ io::open_flags::path | io::open_flags::directory);
+ return true;
+ }
+ if (errno != ENOENT)
+ return false;
+
+ auto parent = path.parent_path();
+ if (parent == path || !parent.has_relative_path())
+ return false;
+
+ unique_fd parent_fd;
+ if (!mkdirs_internal(root, parent, mode, &parent_fd))
+ return false;
+ auto name = path.filename();
+ if (mkdirat(parent_fd.get(), name.c_str(), mode))
+ return false;
+ if (out)
+ *out = io::openat(parent_fd.get(), name,
+ io::open_flags::path | io::open_flags::directory);
+ return true;
+}
+
+} // namespace
+
+namespace io {
+
+bool read_all(int fd, void* data, size_t size) {
+ auto* d = reinterpret_cast<char*>(data);
+ size_t offset = 0;
+ while (offset < size) {
+ auto got = read(fd, d + offset, size - offset);
+ if (got < 0) {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ if (got == 0)
+ return false;
+ offset += got;
+ }
+ return true;
+}
+
+bool write_all(int fd, void const* data, size_t size) {
+ auto* d = reinterpret_cast<char const*>(data);
+ size_t offset = 0;
+ while (offset < size) {
+ auto wrote = write(fd, d + offset, size - offset);
+ if (wrote < 0) {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ if (wrote == 0)
+ return false;
+ offset += wrote;
+ }
+ return true;
+}
+
+Return fill(int fd, Buffer* out, size_t buf_request_size,
+ size_t* bytes) {
+ if (bytes)
+ *bytes = 0;
+ while (true) {
+ size_t avail;
+ auto* ptr = out->wbuf(buf_request_size, avail);
+ if (avail == 0)
+ return Return::OK;
+ auto got = read(fd, ptr, avail);
+ if (got < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return Return::OK;
+ return Return::ERR;
+ }
+ if (got == 0)
+ return Return::CLOSED;
+ if (bytes)
+ *bytes += got;
+ out->wcommit(got);
+ // No point in trying again, will most likely get EAGAIN or EWOULDBLOCK.
+ // Might also be because the connection got closed, but we are in no hurry
+ // to find out so worth the not extra syscall at every read.
+ if (static_cast<size_t>(got) < avail)
+ return Return::OK;
+ }
+}
+
+bool drain(RoBuffer* in, int fd, size_t* bytes) {
+ if (bytes)
+ *bytes = 0;
+ while (true) {
+ size_t avail;
+ auto* ptr = in->rbuf(0, avail);
+ if (avail == 0)
+ return true;
+ auto wrote = write(fd, ptr, avail);
+ if (wrote < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return true;
+ return false;
+ }
+ if (wrote == 0)
+ return false;
+ if (bytes)
+ *bytes += wrote;
+ in->rcommit(wrote);
+ // No point in trying again, will most likely get EAGAIN or EWOULDBLOCK
+ if (static_cast<size_t>(wrote) < avail)
+ return true;
+ }
+}
+
+bool mkdirs(std::filesystem::path const& path, mode_t mode) {
+ return mkdirs(AT_FDCWD, path, mode);
+}
+
+bool mkdirs(int fd, std::filesystem::path const& path, mode_t mode) {
+ return mkdirs_internal(fd, path, mode, nullptr);
+}
+
+bool make_nonblocking(int fd) {
+ int status = fcntl(fd, F_GETFL, 0);
+ if (status == -1)
+ return false;
+ if ((status & O_NONBLOCK) == 0) {
+ if (fcntl(fd, F_SETFL, status | O_NONBLOCK))
+ return false;
+ }
+ return true;
+}
+
+unique_fd open(std::filesystem::path const& path, open_flags flags,
+ std::filesystem::perms perms) {
+ return openat(AT_FDCWD, path, flags, perms);
+}
+
+unique_fd openat(int fd, std::filesystem::path const& path, open_flags flags,
+ std::filesystem::perms perms) {
+ int posix_flags = O_RDONLY;
+ if ((flags & open_flags::wronly) == open_flags::wronly)
+ posix_flags |= O_WRONLY;
+ if ((flags & open_flags::rdwr) == open_flags::rdwr)
+ posix_flags |= O_RDWR;
+ if ((flags & open_flags::create) == open_flags::create)
+ posix_flags |= O_CREAT;
+ if ((flags & open_flags::excl) == open_flags::excl)
+ posix_flags |= O_EXCL;
+ if ((flags & open_flags::trunc) == open_flags::trunc)
+ posix_flags |= O_TRUNC;
+ if ((flags & open_flags::append) == open_flags::append)
+ posix_flags |= O_APPEND;
+ if ((flags & open_flags::directory) == open_flags::directory)
+ posix_flags |= O_DIRECTORY;
+ if ((flags & open_flags::path) == open_flags::path)
+ posix_flags |= O_PATH;
+ return unique_fd(::openat(fd, path.c_str(), posix_flags,
+ static_cast<mode_t>(perms)));
+}
+
+bool close(int fd) {
+ return ::close(fd) == 0;
+}
+
+ssize_t pread(int fd, void *buf, size_t count, off_t offset) {
+ return ::pread(fd, buf, count, offset);
+}
+
+ssize_t read(int fd, void *buf, size_t count) {
+ return ::read(fd, buf, count);
+}
+
+ssize_t write(int fd, const void *buf, size_t count) {
+ return ::write(fd, buf, count);
+}
+
+bool access(std::filesystem::path const& path, access_mode mode) {
+ int posix_mode;
+ if (mode == access_mode::exists) {
+ posix_mode = F_OK;
+ } else {
+ posix_mode = 0;
+ if ((mode & access_mode::read) == access_mode::read)
+ posix_mode |= R_OK;
+ if ((mode & access_mode::write) == access_mode::write)
+ posix_mode |= W_OK;
+ if ((mode & access_mode::exec) == access_mode::exec)
+ posix_mode |= X_OK;
+ }
+ return ::access(path.c_str(), posix_mode) == 0;
+}
+
+bool unlinkat(int fd, std::filesystem::path const& path) {
+ return ::unlinkat(fd, path.c_str(), 0) == 0;
+}
+
+bool fallocate(int fd, off_t offset, off_t len) {
+ return posix_fallocate(fd, offset, len) == 0;
+}
+
+} // namespace io
diff --git a/src/io.hh b/src/io.hh
new file mode 100644
index 0000000..f13b51b
--- /dev/null
+++ b/src/io.hh
@@ -0,0 +1,113 @@
+#ifndef IO_HH
+#define IO_HH
+
+#include "unique_fd.hh"
+
+#include <filesystem>
+#include <stdint.h>
+
+class Buffer;
+class RoBuffer;
+
+namespace io {
+
+bool make_nonblocking(int fd);
+
+bool mkdirs(std::filesystem::path const& path, mode_t mode);
+bool mkdirs(int fd, std::filesystem::path const& path, mode_t mode);
+
+bool read_all(int fd, void* data, size_t size);
+bool write_all(int fd, void const* data, size_t size);
+bool seek_all(int fd, size_t bytes);
+
+enum class Return {
+ OK,
+ ERR,
+ CLOSED,
+};
+
+Return fill(int fd, Buffer* out, size_t buf_request_size = 1,
+ size_t* bytes = nullptr);
+// Returns false in case of error. EAGAIN or EWOULDBLOCK are not errors.
+bool drain(RoBuffer* in, int fd, size_t* bytes = nullptr);
+
+enum class open_flags : unsigned {
+ rdonly = 00,
+ wronly = 01,
+ rdwr = 02,
+ create = 0100,
+ excl = 0200,
+ trunc = 01000,
+ append = 02000,
+ directory = 0200000,
+ path = 010000000,
+};
+
+unique_fd open(std::filesystem::path const& path, open_flags flags,
+ std::filesystem::perms perms = std::filesystem::perms::none);
+unique_fd openat(int fd, std::filesystem::path const& path, open_flags flags,
+ std::filesystem::perms perms = std::filesystem::perms::none);
+
+bool close(int fd);
+
+ssize_t pread(int fd, void *buf, size_t count, off_t offset);
+ssize_t read(int fd, void *buf, size_t count);
+ssize_t write(int fd, const void *buf, size_t count);
+
+enum class access_mode : unsigned {
+ exists = 00,
+ read = 01,
+ write = 02,
+ exec = 04,
+};
+
+bool access(std::filesystem::path const& path, access_mode mode);
+bool unlinkat(int fd, std::filesystem::path const& path);
+bool fallocate(int fd, off_t offset, off_t len);
+
+constexpr open_flags operator&(open_flags a, open_flags b) noexcept {
+ using utype = typename std::underlying_type<open_flags>::type;
+ return static_cast<open_flags>(static_cast<utype>(a) & static_cast<utype>(b));
+}
+
+constexpr open_flags operator|(open_flags a, open_flags b) noexcept {
+ using utype = typename std::underlying_type<open_flags>::type;
+ return static_cast<open_flags>(static_cast<utype>(a) | static_cast<utype>(b));
+}
+
+constexpr open_flags operator^(open_flags a, open_flags b) noexcept {
+ using utype = typename std::underlying_type<open_flags>::type;
+ return static_cast<open_flags>(static_cast<utype>(a) ^ static_cast<utype>(b));
+}
+
+constexpr open_flags operator~(open_flags a) noexcept {
+ using utype = typename std::underlying_type<open_flags>::type;
+ return static_cast<open_flags>(~static_cast<utype>(a));
+}
+
+constexpr access_mode operator&(access_mode a, access_mode b) noexcept {
+ using utype = typename std::underlying_type<access_mode>::type;
+ return static_cast<access_mode>(
+ static_cast<utype>(a) & static_cast<utype>(b));
+}
+
+constexpr access_mode operator|(access_mode a, access_mode b) noexcept {
+ using utype = typename std::underlying_type<access_mode>::type;
+ return static_cast<access_mode>(
+ static_cast<utype>(a) | static_cast<utype>(b));
+}
+
+constexpr access_mode operator^(access_mode a, access_mode b) noexcept {
+ using utype = typename std::underlying_type<access_mode>::type;
+ return static_cast<access_mode>(
+ static_cast<utype>(a) ^ static_cast<utype>(b));
+}
+
+constexpr access_mode operator~(access_mode a) noexcept {
+ using utype = typename std::underlying_type<access_mode>::type;
+ return static_cast<access_mode>(~static_cast<utype>(a));
+}
+
+} // namespace io
+
+#endif // IO_HH
diff --git a/src/jsutil.cc b/src/jsutil.cc
new file mode 100644
index 0000000..ca74b48
--- /dev/null
+++ b/src/jsutil.cc
@@ -0,0 +1,72 @@
+#include "common.hh"
+
+#include "jsutil.hh"
+
+namespace js {
+
+namespace {
+
+constexpr const char kSingleQuoteChars[] = "'\0\\\n\r\v\t\b\f";
+constexpr const char kDoubleQuoteChars[] = "\"\0\\\n\r\v\t\b\f";
+
+} // namespace
+
+std::string quote(std::string_view in, QuoteChar quote_char) {
+ std::string ret;
+ quote(in, ret, quote_char);
+ return ret;
+}
+
+void quote(std::string_view in, std::string& out, QuoteChar quote_char) {
+ out.reserve(out.size() + 2 + in.size());
+ std::string_view chars;
+ switch (quote_char) {
+ case QuoteChar::SINGLE:
+ chars = std::string_view(kSingleQuoteChars, sizeof(kSingleQuoteChars) - 1);
+ break;
+ case QuoteChar::DOUBLE:
+ chars = std::string_view(kDoubleQuoteChars, sizeof(kDoubleQuoteChars) - 1);
+ break;
+ }
+ out.push_back(chars.front());
+ size_t last = 0;
+ while (true) {
+ auto next = in.find_first_of(chars, last);
+ if (next == std::string::npos) {
+ out.append(in, last);
+ break;
+ }
+ out.append(in, last, next - last);
+ out.push_back('\\');
+ switch (in[next]) {
+ case '\0':
+ out.push_back('0');
+ break;
+ case '\n':
+ out.push_back('n');
+ break;
+ case '\r':
+ out.push_back('r');
+ break;
+ case '\v':
+ out.push_back('v');
+ break;
+ case '\t':
+ out.push_back('t');
+ break;
+ case '\b':
+ out.push_back('b');
+ break;
+ case '\f':
+ out.push_back('f');
+ break;
+ default:
+ out.push_back(in[next]);
+ break;
+ }
+ last = next + 1;
+ }
+ out.push_back(chars.front());
+}
+
+} // namespace js
diff --git a/src/jsutil.hh b/src/jsutil.hh
new file mode 100644
index 0000000..ff9582e
--- /dev/null
+++ b/src/jsutil.hh
@@ -0,0 +1,22 @@
+#ifndef JSUTIL_HH
+#define JSUTIL_HH
+
+#include <string>
+#include <string_view>
+
+namespace js {
+
+enum class QuoteChar {
+ SINGLE,
+ DOUBLE,
+};
+
+
+std::string quote(std::string_view in,
+ QuoteChar quote_char = QuoteChar::DOUBLE);
+void quote(std::string_view in, std::string& out,
+ QuoteChar quote_char = QuoteChar::DOUBLE);
+
+} // namespace js
+
+#endif // JSUTIL_HH
diff --git a/src/location.hh b/src/location.hh
new file mode 100644
index 0000000..ce4984e
--- /dev/null
+++ b/src/location.hh
@@ -0,0 +1,23 @@
+#ifndef LOCATION_HH
+#define LOCATION_HH
+
+#include <limits>
+#include <math.h>
+
+struct Location {
+ double lat;
+ double lng;
+
+ Location(double lat, double lng)
+ : lat(lat), lng(lng) {}
+
+ Location()
+ : lat(std::numeric_limits<double>::quiet_NaN()),
+ lng(std::numeric_limits<double>::quiet_NaN()) {}
+
+ bool empty() const {
+ return std::isnan(lat) || std::isnan(lng);
+ }
+};
+
+#endif // LOCATION_HH
diff --git a/src/logger.hh b/src/logger.hh
new file mode 100644
index 0000000..47bbedd
--- /dev/null
+++ b/src/logger.hh
@@ -0,0 +1,57 @@
+#ifndef LOGGER_HH
+#define LOGGER_HH
+
+#include <filesystem>
+#include <memory>
+#include <string>
+
+class Logger {
+public:
+ virtual ~Logger() = default;
+
+ static std::unique_ptr<Logger> create_stdio();
+ // Can return nullptr, will write reason to fallback.
+ static std::unique_ptr<Logger> create_file(std::filesystem::path const& path,
+ Logger* fallback);
+ static std::unique_ptr<Logger> create_syslog(std::string const& prgname);
+ static std::unique_ptr<Logger> create_null();
+
+ virtual void err(char const* format, ...)
+#if HAVE_ATTRIBUTE_FORMAT
+ __attribute__((format(printf, 2, 3))) // this takes up 1
+#endif // HAVE_ATTRIBUTE_FORMAT
+ = 0;
+
+ virtual void warn(char const* format, ...)
+#if HAVE_ATTRIBUTE_FORMAT
+ __attribute__((format(printf, 2, 3)))
+#endif // HAVE_ATTRIBUTE_FORMAT
+ = 0;
+
+ virtual void info(char const* format, ...)
+#if HAVE_ATTRIBUTE_FORMAT
+ __attribute__((format(printf, 2, 3)))
+#endif // HAVE_ATTRIBUTE_FORMAT
+ = 0;
+
+#ifdef NDEBUG
+ void dbg(char const* format, ...)
+#if HAVE_ATTRIBUTE_FORMAT
+ __attribute__((format(printf, 2, 3)))
+#endif // HAVE_ATTRIBUTE_FORMAT
+ {}
+#else
+ virtual void dbg(char const* format, ...)
+#if HAVE_ATTRIBUTE_FORMAT
+ __attribute__((format(printf, 2, 3)))
+#endif // HAVE_ATTRIBUTE_FORMAT
+ = 0;
+#endif
+
+protected:
+ Logger() = default;
+ Logger(Logger const&) = delete;
+ Logger& operator=(Logger const&) = delete;
+};
+
+#endif // LOGGER_HH
diff --git a/src/logger_base.cc b/src/logger_base.cc
new file mode 100644
index 0000000..51f13b9
--- /dev/null
+++ b/src/logger_base.cc
@@ -0,0 +1,69 @@
+#include "common.hh"
+
+#include "logger_base.hh"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+void LoggerBase::err(char const* format, ...) {
+ va_list args;
+ va_start(args, format);
+ char* tmp = nullptr;
+ auto len = vasprintf(&tmp, format, args);
+ va_end(args);
+ if (len == -1) {
+ assert(false);
+ return;
+ }
+ while (len > 0 && tmp[len - 1] == '\n') --len;
+ msg(Level::ERR, std::string_view(tmp, len));
+ free(tmp);
+}
+
+void LoggerBase::warn(char const* format, ...) {
+ va_list args;
+ va_start(args, format);
+ char* tmp = nullptr;
+ auto len = vasprintf(&tmp, format, args);
+ va_end(args);
+ if (len == -1) {
+ assert(false);
+ return;
+ }
+ while (len > 0 && tmp[len - 1] == '\n') --len;
+ msg(Level::WARN, std::string_view(tmp, len));
+ free(tmp);
+}
+
+void LoggerBase::info(char const* format, ...) {
+ va_list args;
+ va_start(args, format);
+ char* tmp = nullptr;
+ auto len = vasprintf(&tmp, format, args);
+ va_end(args);
+ if (len == -1) {
+ assert(false);
+ return;
+ }
+ while (len > 0 && tmp[len - 1] == '\n') --len;
+ msg(Level::INFO, std::string_view(tmp, len));
+ free(tmp);
+}
+
+#ifndef NDEBUG
+void LoggerBase::dbg(char const* format, ...) {
+ va_list args;
+ va_start(args, format);
+ char* tmp = nullptr;
+ auto len = vasprintf(&tmp, format, args);
+ va_end(args);
+ if (len == -1) {
+ assert(false);
+ return;
+ }
+ while (len > 0 && tmp[len - 1] == '\n') --len;
+ msg(Level::DBG, std::string_view(tmp, len));
+ free(tmp);
+}
+#endif
diff --git a/src/logger_base.hh b/src/logger_base.hh
new file mode 100644
index 0000000..feed8ef
--- /dev/null
+++ b/src/logger_base.hh
@@ -0,0 +1,25 @@
+#ifndef LOGGER_BASE_HH
+#define LOGGER_BASE_HH
+
+#include "logger.hh"
+
+#include <string_view>
+
+class LoggerBase : public Logger {
+public:
+ void err(char const* format, ...) override;
+ void warn(char const* format, ...) override;
+ void info(char const* format, ...) override;
+#ifndef NDEBUG
+ void dbg(char const* format, ...) override;
+#endif
+
+protected:
+ enum class Level {
+ ERR, WARN, INFO, DBG
+ };
+
+ virtual void msg(Level level, std::string_view str) = 0;
+};
+
+#endif // LOGGER_BASE_HH
diff --git a/src/logger_file.cc b/src/logger_file.cc
new file mode 100644
index 0000000..ad11398
--- /dev/null
+++ b/src/logger_file.cc
@@ -0,0 +1,52 @@
+#include "common.hh"
+
+#include "logger_base.hh"
+
+#include <fstream>
+#include <mutex>
+
+namespace {
+
+class LoggerFile : public LoggerBase {
+public:
+ explicit LoggerFile(std::filesystem::path const& path)
+ : out_(path, std::ios::out | std::ios::app) {}
+
+ bool good() const {
+ return out_.good();
+ }
+
+protected:
+ void msg(Level lvl, std::string_view msg) override {
+ std::lock_guard<std::mutex> lock(mutex_);
+ switch (lvl) {
+ case Level::ERR:
+ out_ << "Error: " << msg << std::endl;
+ break;
+ case Level::WARN:
+ out_ << "Warning: " << msg << std::endl;
+ break;
+ case Level::INFO:
+ out_ << msg << std::endl;
+ break;
+ case Level::DBG:
+ out_ << "Debug: " << msg << std::endl;
+ break;
+ }
+ }
+
+private:
+ std::mutex mutex_;
+ std::fstream out_;
+};
+
+} // namespace
+
+std::unique_ptr<Logger> Logger::create_file(std::filesystem::path const& path,
+ Logger* fallback) {
+ auto logger = std::make_unique<LoggerFile>(path);
+ if (logger->good())
+ return logger;
+ fallback->warn("Unable to open %s for appending.", path.c_str());
+ return nullptr;
+}
diff --git a/src/logger_null.cc b/src/logger_null.cc
new file mode 100644
index 0000000..a49db7b
--- /dev/null
+++ b/src/logger_null.cc
@@ -0,0 +1,20 @@
+#include "common.hh"
+
+#include "logger_base.hh"
+
+namespace {
+
+class LoggerNull : public LoggerBase {
+public:
+ LoggerNull() = default;
+
+protected:
+ void msg(Level, std::string_view) override {
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Logger> Logger::create_null() {
+ return std::make_unique<LoggerNull>();
+}
diff --git a/src/logger_stdio.cc b/src/logger_stdio.cc
new file mode 100644
index 0000000..17490a3
--- /dev/null
+++ b/src/logger_stdio.cc
@@ -0,0 +1,41 @@
+#include "common.hh"
+
+#include "logger_base.hh"
+
+#include <iostream>
+#include <mutex>
+
+namespace {
+
+class LoggerStdio : public LoggerBase {
+public:
+ LoggerStdio() = default;
+
+protected:
+ void msg(Level lvl, std::string_view msg) override {
+ std::lock_guard<std::mutex> lock(mutex_);
+ switch (lvl) {
+ case Level::ERR:
+ std::cerr << "Error: " << msg << std::endl;
+ break;
+ case Level::WARN:
+ std::cout << "Warning: " << msg << std::endl;
+ break;
+ case Level::INFO:
+ std::cout << msg << std::endl;
+ break;
+ case Level::DBG:
+ std::cout << "Debug: " << msg << std::endl;
+ break;
+ }
+ }
+
+private:
+ std::mutex mutex_;
+};
+
+} // namespace
+
+std::unique_ptr<Logger> Logger::create_stdio() {
+ return std::make_unique<LoggerStdio>();
+}
diff --git a/src/logger_syslog.cc b/src/logger_syslog.cc
new file mode 100644
index 0000000..684405c
--- /dev/null
+++ b/src/logger_syslog.cc
@@ -0,0 +1,45 @@
+#include "common.hh"
+
+#include "logger_base.hh"
+
+#include <string>
+#include <syslog.h>
+
+namespace {
+
+class LoggerSyslog : public LoggerBase {
+public:
+ explicit LoggerSyslog(std::string const& prgname) {
+ openlog(prgname.c_str(), LOG_PID, LOG_DAEMON);
+ }
+
+ ~LoggerSyslog() override {
+ closelog();
+ }
+
+protected:
+ void msg(Level lvl, std::string_view msg) override {
+ int prio;
+ switch (lvl) {
+ case Level::ERR:
+ prio = LOG_ERR;
+ break;
+ case Level::WARN:
+ prio = LOG_WARNING;
+ break;
+ case Level::INFO:
+ prio = LOG_INFO;
+ break;
+ case Level::DBG:
+ prio = LOG_DEBUG;
+ break;
+ }
+ syslog(prio, "%.*s", static_cast<int>(msg.length()), msg.data());
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Logger> Logger::create_syslog(std::string const& prgname) {
+ return std::make_unique<LoggerSyslog>(prgname);
+}
diff --git a/src/looper.hh b/src/looper.hh
new file mode 100644
index 0000000..3286cb1
--- /dev/null
+++ b/src/looper.hh
@@ -0,0 +1,38 @@
+#ifndef LOOPER_HH
+#define LOOPER_HH
+
+#include <functional>
+#include <memory>
+
+class Logger;
+
+class Looper {
+public:
+ constexpr static uint8_t EVENT_READ = 1;
+ constexpr static uint8_t EVENT_WRITE = 2;
+ constexpr static uint8_t EVENT_ERROR = 4;
+
+ virtual ~Looper() = default;
+
+ static std::unique_ptr<Looper> create();
+
+ virtual void add(int fd, uint8_t events,
+ std::function<void(uint8_t)> callback) = 0;
+ virtual void update(int fd, uint8_t events) = 0;
+ virtual void remove(int fd) = 0;
+
+ // Returned id is never 0
+ virtual uint32_t schedule(double delay,
+ std::function<void(uint32_t)> callback) = 0;
+ virtual void cancel(uint32_t id) = 0;
+
+ virtual bool run(Logger* logger) = 0;
+ virtual void quit() = 0;
+
+protected:
+ Looper() = default;
+ Looper(Looper const&) = delete;
+ Looper& operator=(Looper const&) = delete;
+};
+
+#endif // LOOPER_HH
diff --git a/src/looper_poll.cc b/src/looper_poll.cc
new file mode 100644
index 0000000..b08a686
--- /dev/null
+++ b/src/looper_poll.cc
@@ -0,0 +1,223 @@
+#include "common.hh"
+
+#include "looper.hh"
+#include "logger.hh"
+
+#include <chrono>
+#include <deque>
+#include <errno.h>
+#include <poll.h>
+#include <string.h>
+#include <unordered_map>
+#include <vector>
+
+namespace {
+
+class LooperPoll : public Looper {
+public:
+ LooperPoll() = default;
+
+ void add(int fd, uint8_t events,
+ std::function<void(uint8_t)> callback) override {
+ if (fd < 0)
+ return;
+ auto ret = entry_.emplace(fd, Entry(events));
+ if (!ret.second) {
+ assert(ret.first->second.delete_);
+ ret.first->second.delete_ = false;
+ ret.first->second.events_ = events;
+ }
+ ret.first->second.callback_ = std::move(callback);
+ }
+
+ void update(int fd, uint8_t events) override {
+ if (fd < 0)
+ return;
+ auto it = entry_.find(fd);
+ if (it == entry_.end() || it->second.delete_) {
+ assert(false);
+ return;
+ }
+ it->second.events_ = events;
+ }
+
+ void remove(int fd) override {
+ if (fd < 0)
+ return;
+ auto it = entry_.find(fd);
+ if (it == entry_.end())
+ return;
+ it->second.delete_ = true;
+ }
+
+ bool run(Logger* logger) override {
+ while (!quit_) {
+ int timeout;
+ if (scheduled_.empty()) {
+ timeout = -1;
+ } else {
+ auto now = std::chrono::steady_clock::now();
+ while (true) {
+ if (now < scheduled_.front().target_) {
+ auto delay = std::chrono::duration_cast<std::chrono::milliseconds>(
+ scheduled_.front().target_ - now);
+ if (delay.count() <= std::numeric_limits<int>::max())
+ timeout = delay.count();
+ else
+ timeout = std::numeric_limits<int>::max();
+ break;
+ }
+ auto id = scheduled_.front().id_;
+ auto callback = std::move(scheduled_.front().callback_);
+ scheduled_.pop_front();
+ callback(id);
+ if (scheduled_.empty()) {
+ timeout = -1;
+ break;
+ }
+ }
+ // Scheduled callbacks might call quit().
+ if (quit_)
+ break;
+ }
+ std::vector<struct pollfd> pollfd;
+ pollfd.reserve(entry_.size());
+ auto it = entry_.begin();
+ while (it != entry_.end()) {
+ if (it->second.delete_) {
+ it = entry_.erase(it);
+ } else {
+ struct pollfd tmp;
+ tmp.fd = it->first;
+ tmp.events = events_looper2poll(it->second.events_);
+ pollfd.push_back(std::move(tmp));
+ ++it;
+ }
+ }
+ int active = poll(pollfd.data(), pollfd.size(), timeout);
+ if (active < 0) {
+ if (errno == EINTR)
+ continue;
+ logger->err("Poll failed: %s", strerror(errno));
+ return false;
+ }
+ for (auto it2 = pollfd.begin(); active; ++it2) {
+ if (it2->revents == 0)
+ continue;
+ --active;
+ auto events = events_poll2looper(it2->revents);
+ if (events) {
+ it = entry_.find(it2->fd);
+ if (!it->second.delete_) {
+ events &= (it->second.events_ | EVENT_ERROR);
+ if (events) {
+ it->second.callback_(events);
+ }
+ }
+ }
+ }
+ }
+ // Reset quit_ so run() can be called again
+ quit_ = false;
+ return true;
+ }
+
+ void quit() override {
+ quit_ = true;
+ }
+
+ uint32_t schedule(double delay,
+ std::function<void(uint32_t)> callback) override {
+ assert(delay >= 0.0);
+ uint32_t id = next_schedule_id();
+ auto target = std::chrono::steady_clock::now() +
+ std::chrono::duration_cast<std::chrono::steady_clock::duration>(
+ std::chrono::duration<double>(delay));
+ auto insert = scheduled_.end();
+ while (insert != scheduled_.begin()) {
+ auto prev = insert - 1;
+ if (prev->target_ < target)
+ break;
+ insert = prev;
+ }
+ scheduled_.emplace(insert, std::move(callback), id, target);
+ return id;
+ }
+
+ void cancel(uint32_t id) override {
+ for (auto it = scheduled_.begin(); it != scheduled_.end(); ++it) {
+ if (it->id_ == id) {
+ scheduled_.erase(it);
+ break;
+ }
+ }
+ }
+
+private:
+ struct Entry {
+ uint8_t events_;
+ std::function<void(uint8_t)> callback_;
+ bool delete_;
+
+ explicit Entry(uint8_t events)
+ : events_(events), delete_(false) {}
+ };
+
+ struct Scheduled {
+ std::function<void(uint32_t)> callback_;
+ uint32_t id_;
+ std::chrono::steady_clock::time_point target_;
+
+ Scheduled(std::function<void(uint32_t)> callback, uint32_t id,
+ std::chrono::steady_clock::time_point target)
+ : callback_(std::move(callback)), id_(id), target_(target) {}
+ };
+
+ static short events_looper2poll(uint8_t events) {
+ short ret = 0;
+ if (events & EVENT_READ)
+ ret |= POLLIN | POLLPRI;
+ if (events & EVENT_WRITE)
+ ret |= POLLOUT;
+ return ret;
+ }
+
+ static uint8_t events_poll2looper(short events) {
+ uint8_t ret = 0;
+ if (events & (POLLIN | POLLPRI | POLLHUP))
+ ret |= EVENT_READ;
+ if (events & POLLOUT)
+ ret |= EVENT_WRITE;
+ if (events & (POLLERR | POLLNVAL))
+ ret |= EVENT_ERROR;
+ return ret;
+ }
+
+ uint32_t next_schedule_id() {
+ while (true) {
+ uint32_t ret = next_schedule_id_++;
+ if (ret) {
+ bool found = false;
+ for (auto const& scheduled : scheduled_) {
+ if (scheduled.id_ == ret) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ return ret;
+ }
+ }
+ }
+
+ bool quit_;
+ std::unordered_map<int, Entry> entry_;
+ uint32_t next_schedule_id_{1};
+ std::deque<Scheduled> scheduled_;
+};
+
+} // namespace
+
+std::unique_ptr<Looper> Looper::create() {
+ return std::make_unique<LooperPoll>();
+}
diff --git a/src/mime_types.cc b/src/mime_types.cc
new file mode 100644
index 0000000..1332f5a
--- /dev/null
+++ b/src/mime_types.cc
@@ -0,0 +1,29 @@
+#include "common.hh"
+
+#include "mime_types.hh"
+
+#include <unordered_map>
+
+namespace mime_types {
+
+namespace {
+
+std::unordered_map<std::string_view, std::string_view> kExtensionMap({
+ { "css", "text/css" },
+ { "jpeg", "image/jpeg" },
+ { "jpg", "image/jpeg" },
+ { "js", "text/javascript" },
+ { "png", "image/png" },
+ { "webp", "image/webp" },
+});
+
+} // namespace
+
+std::string_view from_extension(std::string_view ext) {
+ auto it = kExtensionMap.find(ext);
+ if (it == kExtensionMap.end())
+ return std::string_view();
+ return it->second;
+}
+
+} // namespace mime_types
diff --git a/src/mime_types.hh b/src/mime_types.hh
new file mode 100644
index 0000000..c68a7f9
--- /dev/null
+++ b/src/mime_types.hh
@@ -0,0 +1,12 @@
+#ifndef MIME_TYPES_HH
+#define MIME_TYPES_HH
+
+#include <string_view>
+
+namespace mime_types {
+
+std::string_view from_extension(std::string_view ext);
+
+} // namespace mime_types
+
+#endif // MIME_TYPES_HH
diff --git a/src/observer_list.hh b/src/observer_list.hh
new file mode 100644
index 0000000..86cdc03
--- /dev/null
+++ b/src/observer_list.hh
@@ -0,0 +1,148 @@
+#ifndef OBSERVER_LIST_HH
+#define OBSERVER_LIST_HH
+
+#include "common.hh"
+
+#include <algorithm>
+#include <vector>
+
+template<typename T>
+class ObserverList {
+public:
+ class iterator {
+ public:
+ iterator()
+ : list_(nullptr), index_(0), end_(0) {}
+
+ ~iterator() {
+ if (list_)
+ list_->release();
+ }
+
+ iterator(iterator const& it)
+ : list_(it.list_), index_(it.index_), end_(it.end_) {
+ if (list_) {
+ list_->aquire();
+ while (index_ < end_ && !list_->observers_[index_])
+ ++index_;
+ }
+ }
+
+ iterator& operator=(iterator const& it) {
+ if (list_ != it.list_) {
+ if (list_)
+ list_->release();
+ list_ = it.list_;
+ if (list_)
+ list_->aquire();
+ }
+ index_ = it.index_;
+ end_ = it.end_;
+ return *this;
+ }
+
+ explicit operator bool() {
+ return index_ < end_;
+ }
+
+ T& operator*() {
+ return *list_->observers_[index_];
+ }
+
+ T* operator->() {
+ return list_->observers_[index_];
+ }
+
+ iterator operator++(int) {
+ iterator ret(*this);
+ ++(*this);
+ return ret;
+ }
+
+ iterator& operator++() {
+ if (index_ < end_) {
+ do {
+ ++index_;
+ } while (index_ < end_ && !list_->observers_[index_]);
+ }
+ return *this;
+ }
+
+ private:
+ friend class ObserverList;
+
+ iterator(ObserverList* list, size_t index, size_t end)
+ : list_(list), index_(index), end_(end) {
+ list_->aquire();
+ while (index_ < end_ && !list_->observers_[index_])
+ ++index_;
+ }
+
+ ObserverList* list_;
+ size_t index_;
+ size_t end_;
+ };
+
+ ObserverList() = default;
+ ~ObserverList() {
+ assert(active_ == 0);
+ }
+
+ bool empty() const {
+ return observers_.empty();
+ }
+
+ void add(T* observer) {
+ assert(std::find(observers_.begin(), observers_.end(), observer)
+ == observers_.end());
+ observers_.push_back(observer);
+ }
+
+ void remove(T* observer) {
+ auto it = std::find(observers_.begin(), observers_.end(), observer);
+ if (it != observers_.end()) {
+ if (active_) {
+ *it = nullptr;
+ ++deleted_;
+ } else {
+ observers_.erase(it);
+ }
+ } else {
+ assert(false);
+ }
+ }
+
+ iterator notify() {
+ return iterator(this, 0, observers_.size());
+ }
+
+private:
+ void aquire() {
+ ++active_;
+ }
+
+ void release() {
+ assert(active_ > 0);
+ --active_;
+ if (active_ == 0 && deleted_)
+ cleanup();
+ }
+
+ void cleanup() {
+ size_t i = observers_.size();
+ while (deleted_ && i > 0) {
+ --i;
+ if (!observers_[i]) {
+ --deleted_;
+ observers_.erase(observers_.begin() + i);
+ }
+ }
+ assert(deleted_ == 0);
+ }
+
+ std::vector<T*> observers_;
+ unsigned active_{0};
+ unsigned deleted_{0};
+};
+
+#endif // OBSERVER_LIST_HH
diff --git a/src/pathutil.cc b/src/pathutil.cc
new file mode 100644
index 0000000..bb2b8e8
--- /dev/null
+++ b/src/pathutil.cc
@@ -0,0 +1,44 @@
+#include "common.hh"
+
+#include "pathutil.hh"
+#include "strutil.hh"
+
+#include <vector>
+
+namespace path {
+
+std::string cleanup(std::string_view path) {
+ auto trimmed_path = str::trim(path);
+ bool trailing = !trimmed_path.empty() && trimmed_path.back() == '/';
+ auto parts = str::split(trimmed_path, '/');
+ auto it = parts.begin();
+ while (it != parts.end()) {
+ if (it->empty() || *it == ".") {
+ if (it + 1 == parts.end())
+ trailing = true;
+ it = parts.erase(it);
+ } else if (*it == "..") {
+ if (it + 1 == parts.end())
+ trailing = true;
+ if (it > parts.begin()) {
+ it = parts.erase(it - 1, it);
+ } else {
+ it = parts.erase(it);
+ }
+ } else {
+ ++it;
+ }
+ }
+ if (parts.empty())
+ return "/";
+ std::string ret;
+ for (auto const& part : parts) {
+ ret.push_back('/');
+ ret.append(part);
+ }
+ if (trailing)
+ ret.push_back('/');
+ return ret;
+}
+
+} // namespace path
diff --git a/src/pathutil.hh b/src/pathutil.hh
new file mode 100644
index 0000000..af2f6fb
--- /dev/null
+++ b/src/pathutil.hh
@@ -0,0 +1,13 @@
+#ifndef PATHUTIL_HH
+#define PATHUTIL_HH
+
+#include <string>
+#include <string_view>
+
+namespace path {
+
+std::string cleanup(std::string_view path);
+
+} // namespace path
+
+#endif // PATHUTIL_HH
diff --git a/src/ro_buffer.cc b/src/ro_buffer.cc
new file mode 100644
index 0000000..ee6208e
--- /dev/null
+++ b/src/ro_buffer.cc
@@ -0,0 +1,29 @@
+#include "common.hh"
+
+#include "ro_buffer.hh"
+
+#include <algorithm>
+
+size_t RoBuffer::read(RoBuffer* buf, void* data, size_t len) {
+ assert(buf);
+ assert(data);
+ if (len == 0)
+ return 0;
+ auto* d = reinterpret_cast<char*>(data);
+ size_t got = 0;
+ while (true) {
+ size_t avail;
+ auto want = len - got;
+ auto* ptr = buf->rbuf(want, avail);
+ if (avail == 0)
+ return got;
+ if (avail >= want) {
+ std::copy_n(ptr, want, d + got);
+ buf->rcommit(want);
+ return len;
+ }
+ std::copy_n(ptr, avail, d + got);
+ buf->rcommit(avail);
+ got += avail;
+ }
+}
diff --git a/src/ro_buffer.hh b/src/ro_buffer.hh
new file mode 100644
index 0000000..a538f78
--- /dev/null
+++ b/src/ro_buffer.hh
@@ -0,0 +1,23 @@
+#ifndef RO_BUFFER_HH
+#define RO_BUFFER_HH
+
+#include <stddef.h>
+
+class RoBuffer {
+public:
+ virtual ~RoBuffer() = default;
+
+ virtual bool empty() const = 0;
+
+ virtual char const* rbuf(size_t want, size_t& avail) = 0;
+ virtual void rcommit(size_t bytes) = 0;
+
+ static size_t read(RoBuffer* buf, void* data, size_t len);
+
+protected:
+ RoBuffer() = default;
+ RoBuffer(RoBuffer const&) = delete;
+ RoBuffer& operator=(RoBuffer const&) = delete;
+};
+
+#endif // RO_BUFFER_HH
diff --git a/src/rotation.hh b/src/rotation.hh
new file mode 100644
index 0000000..2646b29
--- /dev/null
+++ b/src/rotation.hh
@@ -0,0 +1,16 @@
+#ifndef ROTATION_HH
+#define ROTATION_HH
+
+enum class Rotation {
+ UNKNOWN,
+ NONE,
+ MIRRORED,
+ ROTATED_180,
+ ROTATED_180_MIRRORED,
+ ROTATED_90,
+ ROTATED_90_MIRRORED,
+ ROTATED_270,
+ ROTATED_270_MIRRORED,
+};
+
+#endif // ROTATION_HH
diff --git a/src/send_file.cc b/src/send_file.cc
new file mode 100644
index 0000000..8611b6f
--- /dev/null
+++ b/src/send_file.cc
@@ -0,0 +1,45 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "send_file.hh"
+
+namespace {
+
+class SendFileImpl : public SendFile {
+public:
+ bool setup(Logger*, Config* config,
+ std::string_view sendfile_header_name,
+ std::string_view sendfile_path_name) override {
+ header_ = config->get(std::string(sendfile_header_name), "");
+ path_ = config->get(std::string(sendfile_path_name), "");
+ return true;
+ }
+
+ std::unique_ptr<Transport::Response> create_ok_file(
+ Transport* transport,
+ std::filesystem::path const& full_path, std::string_view relative_path,
+ std::string_view etag, std::optional<uint64_t> size) override {
+ if (header_.empty()) {
+ auto resp = transport->create_ok_file(full_path);
+ if (!etag.empty())
+ resp->add_header("ETag", std::string(etag));
+ if (size.has_value())
+ resp->add_header("Content-Length", std::to_string(size.value()));
+ return resp;
+ }
+
+ auto resp = transport->create_ok_data("");
+ resp->add_header(header_, path_ + std::string(relative_path));
+ return resp;
+ }
+
+private:
+ std::string header_;
+ std::string path_;
+};
+
+} // namespace
+
+std::unique_ptr<SendFile> SendFile::create() {
+ return std::make_unique<SendFileImpl>();
+}
diff --git a/src/send_file.hh b/src/send_file.hh
new file mode 100644
index 0000000..aca21b6
--- /dev/null
+++ b/src/send_file.hh
@@ -0,0 +1,35 @@
+#ifndef SEND_FILE_HH
+#define SEND_FILE_HH
+
+#include "transport.hh"
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <string_view>
+
+class Logger;
+class Config;
+
+class SendFile {
+public:
+ virtual ~SendFile() = default;
+
+ static std::unique_ptr<SendFile> create();
+
+ virtual bool setup(Logger* logger, Config* config,
+ std::string_view sendfile_header_name,
+ std::string_view sendfile_path_name) = 0;
+
+ virtual std::unique_ptr<Transport::Response> create_ok_file(
+ Transport* transport, std::filesystem::path const& full_path,
+ std::string_view relative_path, std::string_view etag,
+ std::optional<uint64_t> size) = 0;
+
+protected:
+ SendFile() = default;
+ SendFile(SendFile const&) = delete;
+ SendFile& operator=(SendFile const&) = delete;
+};
+
+#endif // SEND_FILE_HH
diff --git a/src/server.cc b/src/server.cc
new file mode 100644
index 0000000..b2d03fa
--- /dev/null
+++ b/src/server.cc
@@ -0,0 +1,256 @@
+#include "common.hh"
+
+#include "args.hh"
+#include "config.hh"
+#include "inet.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "signal_handler.hh"
+#include "site.hh"
+#include "task_runner.hh"
+#include "transport.hh"
+#include "transport_fastcgi.hh"
+#include "transport_http.hh"
+#include "travel.hh"
+
+#include <algorithm>
+#include <errno.h>
+#include <iostream>
+#include <map>
+#include <signal.h>
+#include <string.h>
+#include <unistd.h>
+#include <utility>
+#include <vector>
+
+#ifndef VERSION
+# warning VERSION not set
+# define VERSION ""
+#endif
+
+namespace {
+
+class Server {
+public:
+ ~Server() {
+ for (auto& fd : listen_)
+ looper_->remove(fd.get());
+ }
+
+ bool setup(Logger* logger, Option const* config_arg, Option const* log_arg,
+ std::function<std::unique_ptr<Logger>()> default_logger_factory) {
+ auto config = Config::create(logger, config_arg->is_set()
+ ? config_arg->arg() : "travel3.conf");
+ if (!config)
+ return false;
+
+ {
+ std::filesystem::path log_file;
+ if (log_arg->is_set()) {
+ log_file = log_arg->arg();
+ } else {
+ log_file = config->get_path("log_file", "");
+ }
+ if (!log_file.empty()) {
+ logger_ = Logger::create_file(log_file, logger);
+ // If a log_file was requested and we can't append to it, treat as
+ // fatal error instead of falling back to default.
+ if (!logger_)
+ return false;
+ } else {
+ logger_ = default_logger_factory();
+ }
+ }
+
+ // EPIPE is handled so SIGPIPE is no help.
+ signal(SIGPIPE, SIG_IGN);
+
+ looper_ = Looper::create();
+ runner_ = TaskRunner::create(looper_);
+ travel_ = Travel::create(logger_, runner_);
+ if (!travel_->setup(logger, config.get()))
+ return false;
+ site_ = Site::create(logger_, runner_, travel_);
+ if (!site_->setup(logger, config.get()))
+ return false;
+
+ {
+ std::map<std::string, std::unique_ptr<Transport::Factory>>
+ transport_factories;
+ transport_factories.emplace("http", create_transport_factory_http());
+ transport_factories.emplace("fastcgi",
+ create_transport_factory_fastcgi());
+
+ auto* transport_factory_name = config->get("transport", "http");
+ auto it = transport_factories.find(transport_factory_name);
+ if (it == transport_factories.end()) {
+ logger->err("Unknown or unsupported transport: `%s'",
+ transport_factory_name);
+ return false;
+ }
+ handler_ = Transport::create_default_handler(logger_, site_->handler());
+ transport_ = it->second->create(logger_, looper_, runner_,
+ logger, config.get(), handler_.get());
+
+ if (!transport_)
+ return false;
+ }
+
+ if (!inet::bind_and_listen(logger,
+ config->get("bind", ""),
+ config->get("port", "5555"),
+ &listen_))
+ return false;
+
+ assert(!listen_.empty());
+
+ for (auto& fd : listen_)
+ looper_->add(fd.get(), Looper::EVENT_READ,
+ std::bind(&Server::accept, this,
+ fd.get(), std::placeholders::_1));
+
+ return true;
+ }
+
+ bool run() {
+ assert(logger_);
+ assert(looper_);
+ assert(transport_);
+
+ if (listen_.empty())
+ return true;
+
+ auto int_handler = SignalHandler::create(
+ looper_, SignalHandler::Signal::INT,
+ std::bind(&Looper::quit, looper_));
+
+ auto term_handler = SignalHandler::create(
+ looper_, SignalHandler::Signal::TERM,
+ std::bind(&Looper::quit, looper_));
+
+ auto hup_handler = SignalHandler::create(
+ looper_, SignalHandler::Signal::HUP,
+ std::bind(&Server::reload, this));
+
+ travel_->start();
+ site_->start();
+
+ return looper_->run(logger_.get());
+ }
+
+private:
+ void reload() {
+ travel_->reload();
+ site_->reload();
+ }
+
+ void accept(int listen_fd, uint8_t event) {
+ if (event & Looper::EVENT_READ) {
+ auto fd = inet::accept(logger_.get(), listen_fd, true);
+ if (fd) {
+ transport_->add_client(std::move(fd));
+ }
+ return;
+ }
+ if (event & Looper::EVENT_ERROR) {
+ looper_->remove(listen_fd);
+ auto it = std::find_if(listen_.begin(), listen_.end(),
+ [listen_fd] (auto& unique_fd) -> bool {
+ return unique_fd.get() == listen_fd;
+ });
+ if (it == listen_.end()) {
+ assert(false);
+ } else {
+ listen_.erase(it);
+ }
+ if (listen_.empty())
+ looper_->quit();
+ return;
+ }
+ // Event should be either read or error
+ assert(false);
+ }
+
+ std::shared_ptr<Logger> logger_;
+ std::shared_ptr<Looper> looper_;
+ std::shared_ptr<TaskRunner> runner_;
+ std::shared_ptr<Travel> travel_;
+ std::unique_ptr<Site> site_;
+ std::unique_ptr<Transport::Handler> handler_;
+ std::unique_ptr<Transport> transport_;
+ std::vector<unique_fd> listen_;
+};
+
+constexpr const char kTryMessage[] = "Try `travel3-server --help` for usage.";
+
+} // namespace
+
+int main(int argc, char** argv) {
+ auto args = Args::create();
+ auto* help_arg = args->add_option('h', "help", "display this text and exit.");
+ auto* version_arg = args->add_option('V', "version",
+ "display version and exit.");
+ auto* daemon_arg = args->add_option(
+ 'D', "daemon", "fork a daemon process, logging to syslog per default");
+ auto* log_arg = args->add_option_with_arg(
+ 'L', "log", "log to FILE instead of default (stdio or syslog)", "FILE");
+ auto* config_arg = args->add_option_with_arg(
+ 'C', "config", "load config from CONFIG instead of default travel3.conf"
+ " in current directory.", "CONFIG");
+ std::vector<std::string> arguments;
+ if (!args->run(argc, argv, "travel3-server", std::cerr, &arguments)) {
+ std::cerr << kTryMessage << std::endl;
+ return EXIT_FAILURE;
+ }
+ if (!arguments.empty()) {
+ std::cerr << "Unexpected arguments.\n"
+ << kTryMessage << std::endl;
+ return EXIT_FAILURE;
+ }
+ if (help_arg->is_set()) {
+ std::cout << "Usage: `travel3-servers [OPTIONS...]'\n"
+ << "Starts the travel server to receive and handle requests.\n"
+ << "\n"
+ << "Options:\n";
+ args->print_descriptions(std::cout, 80);
+ return EXIT_SUCCESS;
+ }
+ if (version_arg->is_set()) {
+ std::cout << "Travel " VERSION " written by "
+ "Joel Klinghed <the_jk@spawned.biz>." << std::endl;
+ return EXIT_SUCCESS;
+ }
+
+ Server server;
+
+ // Setup errors will always be logged to stdio to make them more visible.
+ auto logger = Logger::create_stdio();
+ if (!server.setup(logger.get(), config_arg, log_arg,
+ [daemon_arg] () {
+ return daemon_arg->is_set()
+ ? Logger::create_syslog("travel3")
+ : Logger::create_stdio();
+ }))
+ return EXIT_FAILURE;
+
+ if (daemon_arg->is_set()) {
+ auto pid = fork();
+ if (pid == -1) {
+ logger->err("Failed to fork(): %s", strerror(errno));
+ return EXIT_FAILURE;
+ }
+ if (pid == 0) {
+ // Daemon process
+ chdir("/");
+ setpgrp();
+ close(STDIN_FILENO);
+ close(STDOUT_FILENO);
+ close(STDERR_FILENO);
+ return server.run() ? EXIT_SUCCESS : EXIT_FAILURE;
+ } else {
+ return EXIT_SUCCESS;
+ }
+ } else {
+ return server.run() ? EXIT_SUCCESS : EXIT_FAILURE;
+ }
+}
diff --git a/src/signal_handler.cc b/src/signal_handler.cc
new file mode 100644
index 0000000..7d64e60
--- /dev/null
+++ b/src/signal_handler.cc
@@ -0,0 +1,75 @@
+#include "common.hh"
+
+#include "io.hh"
+#include "looper.hh"
+#include "signal_handler.hh"
+#include "unique_pipe.hh"
+
+#include <signal.h>
+#include <unordered_map>
+
+namespace {
+
+std::unordered_map<int, int> g_fds;
+
+int signum(SignalHandler::Signal signal) {
+ switch (signal) {
+ case SignalHandler::Signal::INT:
+ return SIGINT;
+ case SignalHandler::Signal::TERM:
+ return SIGTERM;
+ case SignalHandler::Signal::HUP:
+ return SIGHUP;
+ }
+ assert(false);
+ return 0;
+}
+
+void signal_handler(int signum) {
+ auto it = g_fds.find(signum);
+ if (it != g_fds.end()) {
+ char c = 1;
+ io::write(it->second, &c, 1);
+ }
+}
+
+class SignalHandlerImpl : public SignalHandler {
+public:
+ SignalHandlerImpl(std::shared_ptr<Looper> looper, Signal signal,
+ std::function<void()> callback)
+ : looper_(looper), signal_(signum(signal)), callback_(std::move(callback)) {
+ looper_->add(pipe_.reader(), Looper::EVENT_READ,
+ std::bind(&SignalHandlerImpl::call, this,
+ std::placeholders::_1));
+ g_fds[signal_] = pipe_.writer();
+
+ ::signal(signal_, signal_handler);
+ }
+
+ ~SignalHandlerImpl() {
+ signal(signal_, SIG_DFL);
+ looper_->remove(pipe_.reader());
+ g_fds.erase(signal_);
+ }
+
+private:
+ void call(uint8_t) {
+ char buf[10];
+ io::read(pipe_.reader(), buf, 10);
+ callback_();
+ }
+
+ unique_pipe pipe_;
+ std::shared_ptr<Looper> looper_;
+ int signal_;
+ std::function<void()> callback_;
+};
+
+} // namespace
+
+std::unique_ptr<SignalHandler> SignalHandler::create(
+ std::shared_ptr<Looper> looper, Signal signal,
+ std::function<void()> callback) {
+ return std::make_unique<SignalHandlerImpl>(
+ std::move(looper), signal, std::move(callback));
+}
diff --git a/src/signal_handler.hh b/src/signal_handler.hh
new file mode 100644
index 0000000..dbbe6d4
--- /dev/null
+++ b/src/signal_handler.hh
@@ -0,0 +1,29 @@
+#ifndef SIGNAL_HANDLER_HH
+#define SIGNAL_HANDLER_HH
+
+#include <functional>
+#include <memory>
+
+class Looper;
+
+class SignalHandler {
+public:
+ virtual ~SignalHandler() = default;
+
+ enum class Signal {
+ INT,
+ TERM,
+ HUP,
+ };
+
+ static std::unique_ptr<SignalHandler> create(std::shared_ptr<Looper> looper,
+ Signal signal,
+ std::function<void()> callback);
+
+protected:
+ SignalHandler() = default;
+ SignalHandler(SignalHandler const&) = delete;
+ SignalHandler& operator=(SignalHandler const&) = delete;
+};
+
+#endif // SIGNAL_HANDLER_HH
diff --git a/src/site.cc b/src/site.cc
new file mode 100644
index 0000000..e1111de
--- /dev/null
+++ b/src/site.cc
@@ -0,0 +1,488 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "document.hh"
+#include "hasher.hh"
+#include "jsutil.hh"
+#include "logger.hh"
+#include "send_file.hh"
+#include "site.hh"
+#include "static_files.hh"
+#include "strutil.hh"
+#include "travel.hh"
+#include "urlutil.hh"
+#include "weak_ptr.hh"
+
+#include <optional>
+#include <unordered_map>
+
+namespace {
+
+const std::string kEmptyGif = std::string(
+ "GIF89a\x01\x00\x01\x00\x00\x00\x00\x21\xf9\x04"
+ "\x01\x0a\x00\x01\x00\x2c\x00\x00\x00\x00\x01\x00"
+ "\x01\x00\x00\x02\x02\x4c\x01\x00\x3b", 0x25);
+
+class SiteImpl : public Site, public Transport::Handler {
+public:
+ SiteImpl(std::shared_ptr<Logger> logger, std::shared_ptr<TaskRunner> runner,
+ std::shared_ptr<Travel> travel)
+ : logger_(std::move(logger)), runner_(std::move(runner)),
+ travel_(std::move(travel)), media_sendfile_(SendFile::create()),
+ static_sendfile_(SendFile::create()), weak_ptr_owner_(this) {}
+
+ bool setup(Logger* logger, Config* config) override {
+ trips_title_ = config->get("site.title", "Travels");
+ trips_index_ = Document::create(trips_title_);
+ trips_index_->add_style("style/base.css");
+ preload_trips_index();
+ static_root_ = config->get_path("site.static_root", "");
+ if (static_root_.empty()) {
+ logger->err(
+ "site.static_root must be set to directory with static files");
+ return false;
+ }
+ auto threads = config->get("site.hasher_threads", 4);
+ if (!threads.has_value()) {
+ logger->err("site.hasher_threads is not a number: %s",
+ config->get("site.hasher_threads", ""));
+ return false;
+ }
+ if (threads.value() <= 0) {
+ logger->err("site.hasher_threads must be > 0");
+ return false;
+ }
+ threads_ = threads.value();
+ if (!media_sendfile_->setup(logger, config,
+ "site.sendfile.header",
+ "site.sendfile.path"))
+ return false;
+ if (!static_sendfile_->setup(logger, config,
+ "site.sendfile.header",
+ "site.sendfile.static_path"))
+ return false;
+ return true;
+ }
+
+ void start() override {
+ assert(instance_ == 0);
+
+ media_hasher_ = Hasher::create(logger_, runner_, threads_);
+
+ do_start();
+ }
+
+ void reload() override {
+ preload_trips_index();
+
+ do_start();
+ }
+
+ Transport::Handler* handler() override {
+ return this;
+ }
+
+ std::unique_ptr<Transport::Response> request(
+ Transport* transport,
+ Transport::Request const* request) override {
+ if (request->path() == "/")
+ return trips_index_->build(transport);
+ auto slash = request->path().find('/', 1);
+ if (slash == std::string::npos) {
+ auto trip_id = request->path().substr(1);
+ if (trip_.count(std::string(trip_id)))
+ return transport->create_redirect(std::string(request->path()) + "/",
+ false);
+ } else {
+ auto trip_id = std::string(request->path().substr(1, slash - 1));
+ auto trip_it = trip_.find(trip_id);
+ if (trip_it != trip_.end()) {
+ auto media_id = std::string(request->path().substr(slash + 1));
+ if (media_id.empty())
+ return trip_it->second.index_->build(transport);
+ if (media_id == "viewer")
+ return trip_it->second.viewer_->build(transport);
+ if (media_id == "thumbnail") {
+ auto idx = str::parse_uint64(
+ std::string(request->query("media")));
+ if (idx.has_value()) {
+ auto thumb_it = trip_it->second.thumbnail_.find(idx.value());
+ if (thumb_it != trip_it->second.thumbnail_.end()) {
+ std::unique_ptr<Transport::Response> ret;
+ switch (thumb_it->second.type_) {
+ case Travel::Thumbnail::ThumbType::FILE:
+ ret = transport->create_ok_file(thumb_it->second.path_);
+ break;
+ case Travel::Thumbnail::ThumbType::EXIF:
+ ret = transport->create_ok_exif_thumbnail(
+ thumb_it->second.path_);
+ break;
+ }
+ if (ret) {
+ ret->add_header("Content-Type",
+ std::string(thumb_it->second.mime_type_));
+ if (!thumb_it->second.etag_.empty())
+ ret->add_header("ETag", thumb_it->second.etag_);
+ ret->add_header("Content-Length",
+ std::to_string(thumb_it->second.size_));
+ return ret;
+ }
+ }
+ return create_empty_image(transport);
+ }
+ return transport->create_not_found();
+ }
+
+ auto media_it = trip_it->second.media_.find(media_id);
+ if (media_it != trip_it->second.media_.end()) {
+ std::string relative = trip_id + "/" + media_id;
+ return media_sendfile_->create_ok_file(transport,
+ media_it->second.path_,
+ relative,
+ media_it->second.etag_,
+ media_it->second.size_);
+ }
+ }
+ }
+
+ auto resp = static_->request(transport, request->path());
+ if (resp)
+ return resp;
+ return transport->create_not_found();
+ }
+
+private:
+ struct Thumbnail {
+ Travel::Thumbnail::ThumbType type_;
+ std::filesystem::path path_;
+ std::string_view mime_type_;
+ std::string etag_;
+ uint64_t size_;
+
+ explicit Thumbnail(Travel::Thumbnail const* thumbnail)
+ : type_(thumbnail->thumb_type()), path_(thumbnail->path()), mime_type_(
+ thumbnail->mime_type()), size_(thumbnail->size()) {}
+ };
+
+ struct Media {
+ std::filesystem::path path_;
+ std::string etag_;
+ std::optional<uint64_t> size_;
+
+ explicit Media(std::filesystem::path path)
+ : path_(std::move(path)) {}
+ };
+
+ struct Trip {
+ std::unique_ptr<Document> index_;
+ std::unique_ptr<Document> viewer_;
+ std::unordered_map<std::string, Media> media_;
+ std::unordered_map<uint64_t, Thumbnail> thumbnail_;
+ };
+
+ static void weak_loaded(std::shared_ptr<WeakPtr<SiteImpl>> weak_ptr,
+ uint16_t instance) {
+ auto* ptr = weak_ptr->get();
+ if (ptr)
+ ptr->loaded(instance);
+ }
+
+ static void weak_hashed_media(std::shared_ptr<WeakPtr<SiteImpl>> weak_ptr,
+ std::string const& trip_id,
+ std::string const& media_id,
+ size_t thumbnail_index,
+ std::string const& hash, uint64_t size) {
+ auto* ptr = weak_ptr->get();
+ if (ptr)
+ ptr->hashed_media(trip_id, media_id, thumbnail_index, hash, size);
+ }
+
+ void do_start() {
+ uint16_t instance = ++instance_;
+
+ static_ = StaticFiles::create(logger_, runner_, static_sendfile_,
+ static_root_, threads_);
+ travel_->call_when_loaded(
+ std::bind(&SiteImpl::weak_loaded, weak_ptr_owner_.get(),
+ instance));
+ }
+
+ void preload_trips_index() {
+ auto* body = trips_index_->body();
+ body->clear_content();
+ body->add_tag("p", "Loading, please wait...");
+ }
+
+ void loaded(uint16_t instance) {
+ if (instance != instance_)
+ return;
+
+ load_trips_index();
+
+ load_trips();
+ }
+
+ void load_trips_index() {
+ auto* body = trips_index_->body();
+ body->clear_content();
+
+ body->add_tag("h1", trips_title_);
+
+ auto* list = body->add_tag("p")->add_tag("ul")->attr("class", "trips");
+ for (size_t i = 0; i < travel_->trips(); ++i) {
+ auto& trip = travel_->trip(i);
+ auto* item = list->add_tag("li")->attr("class", "trip");
+ item->add_tag(
+ "a",
+ std::string(trip.title()) + " - " + std::to_string(trip.year()))
+ ->attr("href", url::escape(trip.id(),
+ url::EscapeFlags::KEEP_SLASH) + "/");
+ item->add_tag("br");
+ std::string extra;
+ if (trip.images() > 0) {
+ if (trip.images() > 1)
+ extra += std::to_string(trip.images()) + " images";
+ else
+ extra += "1 image";
+ }
+ if (trip.videos() > 0) {
+ if (!extra.empty())
+ extra += ", ";
+ if (trip.videos() > 1)
+ extra += std::to_string(trip.videos()) + " videos";
+ else
+ extra += "1 video";
+ }
+ item->add_tag("span", extra)->attr("class", "subtitle");
+ if (!trip.location().empty()) {
+ item->add(" ");
+ item->add_tag("a", "Map")
+ ->attr("class", "maps")
+ ->attr("href", maps_url(trip.location()))
+ ->attr("target", "_blank");
+ }
+ }
+ }
+
+ void load_trips() {
+ trip_.clear();
+
+ for (size_t i = 0; i < travel_->trips(); ++i) {
+ load_trip(travel_->trip(i));
+ }
+ }
+
+ void load_trip_index(Travel::Trip const& trip, Trip& site_trip) {
+ site_trip.index_ = Document::create(std::string(trip.title()));
+ site_trip.index_->add_style("../style/base.css");
+
+ auto* body = site_trip.index_->body();
+
+ body->add_tag("h1", std::string(trip.title()));
+
+ auto* list = body->add_tag("p")->add_tag("ul")->attr("class", "days");
+ for (size_t i = 0; i < trip.day_count(); ++i) {
+ auto& day = trip.day(i);
+ auto* item = list->add_tag("li")->attr("class", "day");
+ item->add_tag("h4", day.date().to_format("%d/%m"));
+ for (size_t j = day.first(); j <= day.last(); ++j) {
+ auto& media = trip.media(j);
+ auto* link = item->add_tag("a");
+ link->attr("href", "viewer?media=" + std::to_string(j));
+ link->attr("title", media.date().to_format("%H:%M"));
+ std::string type;
+ switch (media.type()) {
+ case Travel::Media::Type::IMAGE:
+ type = "image";
+ break;
+ case Travel::Media::Type::VIDEO:
+ type = "video";
+ break;
+ }
+ auto* img = link->add_tag("img")->attr("class", "thumbnail " + type)
+ ->attr("width", "64")->attr("height", "64");
+ if (media.thumbnail())
+ img->attr("src", "thumbnail?media=" + std::to_string(j));
+ item->add(" ");
+ }
+ }
+ }
+
+ void load_trip_viewer(Travel::Trip const& trip, Trip& site_trip) {
+ site_trip.viewer_ = Document::create(std::string(trip.title()));
+ site_trip.viewer_->add_style("../style/base.css");
+ site_trip.viewer_->add_style("../style/viewer.css");
+ site_trip.viewer_->add_script("../js/media.js");
+
+ {
+ auto script = Tag::create("script");
+ script->attr("type", "text/javascript");
+ std::string content = "\n";
+ for (size_t i = 0; i < trip.media_count(); ++i) {
+ auto& media = trip.media(i);
+ std::vector<std::string> args;
+ site_trip.media_.emplace(media.id(), media.path());
+ if (media.thumbnail())
+ site_trip.thumbnail_.emplace(i, media.thumbnail());
+ media_hasher_->hash(media.path(),
+ std::bind(&SiteImpl::weak_hashed_media,
+ weak_ptr_owner_.get(),
+ std::string(trip.id()),
+ std::string(media.id()),
+ i,
+ std::placeholders::_1,
+ std::placeholders::_2));
+ args.push_back(js::quote(url::escape(media.id(),
+ url::EscapeFlags::KEEP_SLASH)));
+ args.push_back(std::to_string(media.width()));
+ args.push_back(std::to_string(media.height()));
+ if (media.location().empty())
+ args.push_back("[]");
+ else
+ args.push_back("[" + std::to_string(media.location().lat) + "," +
+ std::to_string(media.location().lng) + "]");
+ args.push_back(
+ "new Date(" + js::quote(
+ media.date().to_format("%Y, %d, %m, %H, %M, %S")) + ")");
+ switch (media.type()) {
+ case Travel::Media::Type::IMAGE:
+ switch (media.rotation()) {
+ case Rotation::UNKNOWN:
+ case Rotation::NONE:
+ args.push_back(js::quote(""));
+ break;
+ case Rotation::MIRRORED:
+ args.push_back(js::quote("scaleX(-1)"));
+ break;
+ case Rotation::ROTATED_90:
+ args.push_back(js::quote("scaleX(-1) rotate(-90deg)"));
+ break;
+ case Rotation::ROTATED_90_MIRRORED:
+ args.push_back(js::quote("rotate(-90deg)"));
+ break;
+ case Rotation::ROTATED_180:
+ args.push_back(js::quote("scaleY(-1)"));
+ break;
+ case Rotation::ROTATED_180_MIRRORED:
+ args.push_back(js::quote("scaleX(-1) scaleY(-1)"));
+ break;
+ case Rotation::ROTATED_270:
+ args.push_back(js::quote("scaleX(-1) rotate(90deg)"));
+ break;
+ case Rotation::ROTATED_270_MIRRORED:
+ args.push_back(js::quote("rotate(90deg)"));
+ break;
+ }
+ content += "media.push(new Image(";
+ str::join(args, ", ", content);
+ content += "));\n";
+ break;
+ case Travel::Media::Type::VIDEO:
+ args.push_back(std::to_string(media.length()));
+ content += "media.push(new Video(";
+ str::join(args, ", ", content);
+ content += "));\n";
+ break;
+ }
+ }
+ script->add(std::move(content));
+ site_trip.viewer_->add_script(std::move(script));
+ }
+
+ auto* body = site_trip.viewer_->body();
+
+ auto* nav = body->add_tag("div")->attr("id", "header")->add_tag("ul");
+ {
+ auto* li = nav->add_tag("li");
+ li->add_tag("a", "D")
+ ->attr("id", "download")
+ ->attr("download", "")
+ ->attr("title", "Download [Shift-D]");
+ li->add_tag("a", "P")->attr("id", "location")
+ ->attr("title", "Show location");
+ }
+ nav->add_tag("li")->add_tag("a", "<")
+ ->attr("id", "previous")
+ ->attr("title", "Previous [Left Arrow]");
+ nav->add_tag("li")->attr("id", "number");
+ nav->add_tag("li")->add_tag("a", ">")
+ ->attr("id", "next")
+ ->attr("title", "Next [Right Arrow]");
+ nav->add_tag("li")->add_tag("a", "X")
+ ->attr("id", "close")
+ ->attr("title", "Close [Escape]");
+ body->add_tag("img")->attr("class", "content");
+ body->add_tag("video")->attr("class", "content")->attr("controls", "");
+ }
+
+ void load_trip(Travel::Trip const& trip) {
+ Trip site_trip;
+
+ load_trip_index(trip, site_trip);
+ load_trip_viewer(trip, site_trip);
+
+ trip_.emplace(trip.id(), std::move(site_trip));
+ }
+
+ static std::string maps_url(Location location) {
+ std::string ret("https://google.com/maps?q=");
+ ret += std::to_string(location.lat);
+ ret.push_back(',');
+ ret += std::to_string(location.lng);
+ return ret;
+ }
+
+ void hashed_media(std::string const& trip_id,
+ std::string const& media_id,
+ size_t thumbnail_index,
+ std::string const& hash, uint64_t size) {
+ if (hash.empty())
+ return;
+
+ auto trip_it = trip_.find(trip_id);
+ if (trip_it == trip_.end())
+ return;
+ auto media_it = trip_it->second.media_.find(media_id);
+ if (media_it == trip_it->second.media_.end())
+ return;
+ media_it->second.etag_ = "\"" + hash + "\"";
+ media_it->second.size_ = size;
+ auto thumbnail_it = trip_it->second.thumbnail_.find(thumbnail_index);
+ if (thumbnail_it == trip_it->second.thumbnail_.end())
+ return;
+ thumbnail_it->second.etag_ = "\"" + hash + "-exif-thumb\"";
+ }
+
+ std::unique_ptr<Transport::Response> create_empty_image(
+ Transport* transport) {
+ auto ret = transport->create_ok_data(kEmptyGif);
+ ret->add_header("Content-Type", "image/gif");
+ ret->add_header("ETag", "\"bb229a48bee31f5d54ca12dc9bd960c63a671f"
+ "0d4be86a054c1d324a44499d96\"");
+ return ret;
+ }
+
+ std::shared_ptr<Logger> logger_;
+ std::shared_ptr<TaskRunner> runner_;
+ std::shared_ptr<Travel> travel_;
+ size_t threads_{1};
+ uint16_t instance_{0};
+ std::string trips_title_;
+ std::unique_ptr<Document> trips_index_;
+ std::unordered_map<std::string, Trip> trip_;
+ std::shared_ptr<SendFile> media_sendfile_;
+ std::unique_ptr<Hasher> media_hasher_;
+ std::filesystem::path static_root_;
+ std::shared_ptr<SendFile> static_sendfile_;
+ std::unique_ptr<StaticFiles> static_;
+ WeakPtrOwner<SiteImpl> weak_ptr_owner_;
+};
+
+} // namespace
+
+std::unique_ptr<Site> Site::create(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ std::shared_ptr<Travel> travel) {
+ return std::make_unique<SiteImpl>(std::move(logger), std::move(runner),
+ std::move(travel));
+}
diff --git a/src/site.hh b/src/site.hh
new file mode 100644
index 0000000..b6a32ff
--- /dev/null
+++ b/src/site.hh
@@ -0,0 +1,35 @@
+#ifndef SITE_HH
+#define SITE_HH
+
+#include "transport.hh"
+
+#include <memory>
+
+class Config;
+class Logger;
+class TaskRunner;
+class Travel;
+
+class Site {
+public:
+ virtual ~Site() = default;
+
+ static std::unique_ptr<Site> create(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ std::shared_ptr<Travel> travel);
+
+ virtual bool setup(Logger* logger, Config* config) = 0;
+
+ virtual void start() = 0;
+
+ virtual void reload() = 0;
+
+ virtual Transport::Handler* handler() = 0;
+
+protected:
+ Site() = default;
+ Site(Site const&) = delete;
+ Site& operator=(Site const&) = delete;
+};
+
+#endif // SITE_HH
diff --git a/src/static_files.cc b/src/static_files.cc
new file mode 100644
index 0000000..7125ee0
--- /dev/null
+++ b/src/static_files.cc
@@ -0,0 +1,113 @@
+#include "common.hh"
+
+#include "files_finder.hh"
+#include "hasher.hh"
+#include "mime_types.hh"
+#include "send_file.hh"
+#include "static_files.hh"
+#include "weak_ptr.hh"
+
+#include <optional>
+
+namespace {
+
+class StaticFilesImpl : public StaticFiles, public FilesFinder::Delegate {
+public:
+ StaticFilesImpl(
+ std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ std::shared_ptr<SendFile> send_file,
+ std::filesystem::path path,
+ size_t threads)
+ : root_(std::move(path)), send_file_(std::move(send_file)),
+ weak_ptr_owner_(this) {
+ finder_ = FilesFinder::create(logger, runner, root_, this, threads);
+ hasher_ = Hasher::create(logger, runner, threads);
+ }
+
+ std::unique_ptr<Transport::Response> request(
+ Transport* transport, std::string_view path) override {
+ auto it = files_.find(std::string(path));
+ if (it == files_.end())
+ return nullptr;
+ auto resp = send_file_->create_ok_file(transport, it->second.path_,
+ it->first, it->second.etag_,
+ it->second.size_);
+ if (!it->second.mime_type_.empty())
+ resp->add_header("Content-Type", it->second.mime_type_);
+ return resp;
+ }
+
+ void file(std::filesystem::path path) override {
+ std::string name("/");
+ name.append(path.filename());
+ auto parent = path.parent_path();
+ while (true) {
+ if (parent == root_ || !parent.has_parent_path())
+ break;
+ name.insert(0, parent.filename());
+ name.insert(0, "/");
+ parent = parent.parent_path();
+ }
+ files_[name].path_ = path;
+ if (path.has_extension())
+ files_[name].mime_type_ = mime_types::from_extension(
+ std::string(path.extension()).substr(1));
+ hasher_->hash(path, std::bind(&StaticFilesImpl::weak_hashed,
+ weak_ptr_owner_.get(), name,
+ std::placeholders::_1,
+ std::placeholders::_2));
+ }
+
+ void done() override {
+ finder_.reset();
+ }
+
+private:
+ struct File {
+ std::filesystem::path path_;
+ std::string mime_type_;
+ std::string etag_;
+ std::optional<uint64_t> size_;
+ };
+
+ static void weak_hashed(std::shared_ptr<WeakPtr<StaticFilesImpl>> weak_ptr,
+ std::string const& name,
+ std::string hash,
+ uint64_t size) {
+ auto* ptr = weak_ptr->get();
+ if (ptr)
+ ptr->hashed(name, std::move(hash), size);
+ }
+
+ void hashed(std::string const& name, std::string hash, uint64_t size) {
+ if (hash.empty())
+ return;
+
+ hash.insert(0, "\"");
+ hash.push_back('"');
+ files_[name].etag_ = std::move(hash);
+ files_[name].size_ = size;
+ }
+
+ std::filesystem::path root_;
+ std::shared_ptr<SendFile> send_file_;
+ std::unordered_map<std::string, File> files_;
+ std::unique_ptr<FilesFinder> finder_;
+ std::unique_ptr<Hasher> hasher_;
+
+ WeakPtrOwner<StaticFilesImpl> weak_ptr_owner_;
+};
+
+} // namespace
+
+std::unique_ptr<StaticFiles> StaticFiles::create(
+ std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ std::shared_ptr<SendFile> send_file,
+ std::filesystem::path path,
+ size_t threads) {
+ return std::make_unique<StaticFilesImpl>(
+ std::move(logger), std::move(runner), std::move(send_file),
+ std::move(path), threads);
+}
diff --git a/src/static_files.hh b/src/static_files.hh
new file mode 100644
index 0000000..ecff987
--- /dev/null
+++ b/src/static_files.hh
@@ -0,0 +1,33 @@
+#ifndef STATIC_FILES_HH
+#define STATIC_FILES_HH
+
+#include "transport.hh"
+
+#include <filesystem>
+#include <memory>
+
+class Logger;
+class SendFile;
+class TaskRunner;
+
+class StaticFiles {
+public:
+ virtual ~StaticFiles() = default;
+
+ static std::unique_ptr<StaticFiles> create(
+ std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner,
+ std::shared_ptr<SendFile> send_file,
+ std::filesystem::path path,
+ size_t threads = 1);
+
+ virtual std::unique_ptr<Transport::Response> request(
+ Transport* transport, std::string_view path) = 0;
+
+protected:
+ StaticFiles() = default;
+ StaticFiles(StaticFiles const&) = delete;
+ StaticFiles& operator=(StaticFiles const&) = delete;
+};
+
+#endif // STATIC_FILES_HH
diff --git a/src/str_buffer.cc b/src/str_buffer.cc
new file mode 100644
index 0000000..6771cad
--- /dev/null
+++ b/src/str_buffer.cc
@@ -0,0 +1,92 @@
+#include "common.hh"
+
+#include "str_buffer.hh"
+
+#include <utility>
+
+namespace {
+
+template<typename T>
+class StringBuffer : public RoBuffer {
+public:
+ explicit StringBuffer(T str)
+ : str_(std::move(str)), offset_(0) {}
+
+ bool empty() const override {
+ return offset_ >= str_.size();
+ }
+
+ char const* rbuf(size_t /* want */, size_t& avail) override {
+ avail = str_.size() - offset_;
+ return str_.data() + offset_;
+ }
+
+ void rcommit(size_t bytes) override {
+ assert(str_.size() - offset_ >= bytes);
+ offset_ += bytes;
+ }
+
+private:
+ T str_;
+ size_t offset_;
+};
+
+class SharedStringBuffer : public Buffer {
+public:
+ explicit SharedStringBuffer(std::shared_ptr<std::string> content)
+ : content_(std::move(content)), read_ptr_(0), write_ptr_(content_->size()) {
+ }
+
+ bool empty() const override {
+ return read_ptr_ >= content_->size();
+ }
+
+ char const* rbuf(size_t /* want */, size_t& avail) override {
+ avail = content_->size() - read_ptr_;
+ return content_->data() + read_ptr_;
+ }
+
+ void rcommit(size_t bytes) override {
+ assert(content_->size() - read_ptr_ >= bytes);
+ read_ptr_ += bytes;
+ }
+
+ bool full() const override {
+ return false;
+ }
+
+ void clear() override {
+ content_->clear();
+ read_ptr_ = write_ptr_ = 0;
+ }
+
+ char* wbuf(size_t request, size_t& avail) override {
+ avail = request;
+ content_->resize(write_ptr_ + request);
+ return content_->data() + write_ptr_;
+ }
+
+ void wcommit(size_t bytes) override {
+ assert(content_->size() - write_ptr_ >= bytes);
+ write_ptr_ += bytes;
+ }
+
+private:
+ std::shared_ptr<std::string> content_;
+ size_t read_ptr_;
+ size_t write_ptr_;
+};
+
+} // namespace
+
+std::unique_ptr<RoBuffer> make_strbuffer(std::string content) {
+ return std::make_unique<StringBuffer<std::string>>(std::move(content));
+}
+
+std::unique_ptr<RoBuffer> make_strbuffer(std::string_view content) {
+ return std::make_unique<StringBuffer<std::string_view>>(content);
+}
+
+std::unique_ptr<Buffer> make_strbuffer(std::shared_ptr<std::string> content) {
+ return std::make_unique<SharedStringBuffer>(std::move(content));
+}
diff --git a/src/str_buffer.hh b/src/str_buffer.hh
new file mode 100644
index 0000000..b791918
--- /dev/null
+++ b/src/str_buffer.hh
@@ -0,0 +1,14 @@
+#ifndef STR_BUFFER_HH
+#define STR_BUFFER_HH
+
+#include "buffer.hh"
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+std::unique_ptr<RoBuffer> make_strbuffer(std::string content);
+std::unique_ptr<RoBuffer> make_strbuffer(std::string_view content);
+std::unique_ptr<Buffer> make_strbuffer(std::shared_ptr<std::string> content);
+
+#endif // STR_BUFFER_HH
diff --git a/src/strutil.cc b/src/strutil.cc
new file mode 100644
index 0000000..adee769
--- /dev/null
+++ b/src/strutil.cc
@@ -0,0 +1,211 @@
+#include "common.hh"
+
+#include "strutil.hh"
+
+#include <errno.h>
+#include <limits>
+#include <stdlib.h>
+
+namespace str {
+
+namespace {
+
+// Sadly strtoul(l) doesn't treat negative values an error but instead returns
+// ULONG_MAX - value which is indistinguable from ULONG_MAX - value the positive
+// way.
+bool is_negative(std::string const& str) {
+ for (auto i = str.begin(); i != str.end(); ++i) {
+ if (!isspace(*i)) {
+ return *i == '-';
+ }
+ }
+ return false;
+}
+
+} // namespace
+
+std::optional<uint16_t> parse_uint16(std::string const& str) {
+ static_assert(sizeof(unsigned long) >= sizeof(uint16_t),
+ "Need unsigned long to be >= uint16_t");
+ if (str.empty() || is_negative(str))
+ return std::nullopt;
+ char* end = nullptr;
+ errno = 0;
+ auto tmp = strtoul(str.c_str(), &end, 10);
+ if (errno || end != str.c_str() + str.size() ||
+ tmp > std::numeric_limits<uint16_t>::max())
+ return std::nullopt;
+ return static_cast<uint16_t>(tmp);
+}
+
+std::optional<uint32_t> parse_uint32(std::string const& str) {
+ static_assert(sizeof(unsigned long long) >= sizeof(uint32_t),
+ "Need unsigned long long to be >= uint32_t");
+ if (str.empty() || is_negative(str))
+ return std::nullopt;
+ char* end = nullptr;
+ errno = 0;
+ auto tmp = strtoull(str.c_str(), &end, 10);
+ if (errno || end != str.c_str() + str.size() ||
+ tmp > std::numeric_limits<uint32_t>::max())
+ return std::nullopt;
+ return static_cast<uint32_t>(tmp);
+}
+
+std::optional<uint64_t> parse_uint64(std::string const& str) {
+ static_assert(sizeof(unsigned long long) >= sizeof(uint64_t),
+ "Need unsigned long long to be >= uint64_t");
+ if (str.empty() || is_negative(str))
+ return std::nullopt;
+ char* end = nullptr;
+ errno = 0;
+ auto tmp = strtoull(str.c_str(), &end, 10);
+ if (errno || end != str.c_str() + str.size() ||
+ tmp > std::numeric_limits<uint64_t>::max())
+ return std::nullopt;
+ return static_cast<uint64_t>(tmp);
+}
+
+std::vector<std::string_view> split(std::string_view str, char delim) {
+ std::vector<std::string_view> ret;
+ size_t last = 0;
+ while (true) {
+ size_t next = str.find(delim, last);
+ if (next == std::string::npos)
+ break;
+ if (next > last)
+ ret.push_back(str.substr(last, next - last));
+ last = next + 1;
+ }
+ if (last < str.size() || ret.empty())
+ ret.push_back(str.substr(last));
+ return ret;
+}
+
+std::vector<std::string> split(std::string const& str, char delim) {
+ std::vector<std::string> ret;
+ size_t last = 0;
+ while (true) {
+ size_t next = str.find(delim, last);
+ if (next == std::string::npos)
+ break;
+ if (next > last)
+ ret.push_back(str.substr(last, next - last));
+ last = next + 1;
+ }
+ if (last < str.size() || ret.empty())
+ ret.push_back(str.substr(last));
+ return ret;
+}
+
+std::string join(std::vector<std::string> const& in, char delim) {
+ std::string ret;
+ join(in, delim, ret);
+ return ret;
+}
+
+std::string join(std::vector<std::string> const& in, std::string_view delim) {
+ std::string ret;
+ join(in, delim, ret);
+ return ret;
+}
+
+void join(std::vector<std::string> const& in, char delim, std::string& out) {
+ join(in, std::string_view(&delim, 1), out);
+}
+
+void join(std::vector<std::string> const& in, std::string_view delim,
+ std::string& out) {
+ auto it = in.begin();
+ if (it == in.end())
+ return;
+ out.append(*it);
+ for (++it; it != in.end(); ++it) {
+ out.append(delim);
+ out.append(*it);
+ }
+}
+
+std::string join(std::vector<std::string_view> const& in, char delim) {
+ std::string ret;
+ join(in, delim, ret);
+ return ret;
+}
+
+std::string join(std::vector<std::string_view> const& in,
+ std::string_view delim) {
+ std::string ret;
+ join(in, delim, ret);
+ return ret;
+}
+
+void join(std::vector<std::string_view> const& in, char delim,
+ std::string& out) {
+ join(in, std::string_view(&delim, 1), out);
+}
+
+void join(std::vector<std::string_view> const& in, std::string_view delim,
+ std::string& out) {
+ auto it = in.begin();
+ if (it == in.end())
+ return;
+ out.append(*it);
+ for (++it; it != in.end(); ++it) {
+ out.append(delim);
+ out.append(*it);
+ }
+}
+
+bool starts_with(std::string_view str, std::string_view prefix) {
+ return str.size() >= prefix.size() &&
+ str.compare(0, prefix.size(), prefix) == 0;
+}
+
+bool ends_with(std::string_view str, std::string_view suffix) {
+ return str.size() >= suffix.size() &&
+ str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
+}
+
+std::string_view trim(std::string_view str) {
+ return ltrim(rtrim(str));
+}
+
+std::string_view ltrim(std::string_view str) {
+ size_t start = 0;
+ while (start < str.size() && str[start] == ' ')
+ ++start;
+ return str.substr(start);
+}
+
+std::string_view rtrim(std::string_view str) {
+ size_t end = str.size();
+ while (end > 0 && str[end - 1] == ' ')
+ --end;
+ return str.substr(0, end);
+}
+
+std::string trim(std::string const& str) {
+ size_t start = 0;
+ while (start < str.size() && str[start] == ' ')
+ ++start;
+ size_t end = str.size();
+ while (end > start && str[end - 1] == ' ')
+ --end;
+ return str.substr(start, end - start);
+}
+
+std::string ltrim(std::string const& str) {
+ size_t start = 0;
+ while (start < str.size() && str[start] == ' ')
+ ++start;
+ return str.substr(start);
+}
+
+std::string rtrim(std::string const& str) {
+ size_t end = str.size();
+ while (end > 0 && str[end - 1] == ' ')
+ --end;
+ return str.substr(0, end);
+}
+
+} // namespace str
diff --git a/src/strutil.hh b/src/strutil.hh
new file mode 100644
index 0000000..28bbf55
--- /dev/null
+++ b/src/strutil.hh
@@ -0,0 +1,51 @@
+#ifndef STRUTIL_HH
+#define STRUTIL_HH
+
+#include <optional>
+#include <stdint.h>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace str {
+
+std::optional<uint16_t> parse_uint16(std::string const& str);
+std::optional<uint32_t> parse_uint32(std::string const& str);
+std::optional<uint64_t> parse_uint64(std::string const& str);
+
+// Empty substrings are ignored but out will always be at least one entry
+std::vector<std::string_view> split(std::string_view str, char delim = ' ');
+
+std::vector<std::string> split(std::string const& str, char delim = ' ');
+
+std::string join(std::vector<std::string> const& in, char delim);
+std::string join(std::vector<std::string> const& in, std::string_view delim);
+
+void join(std::vector<std::string> const& in, char delim, std::string& out);
+void join(std::vector<std::string> const& in, std::string_view delim,
+ std::string& out);
+
+std::string join(std::vector<std::string_view> const& in, char delim);
+std::string join(std::vector<std::string_view> const& in,
+ std::string_view delim);
+
+void join(std::vector<std::string_view> const& in, char delim,
+ std::string& out);
+void join(std::vector<std::string_view> const& in, std::string_view delim,
+ std::string& out);
+
+[[nodiscard]] std::string_view trim(std::string_view str);
+[[nodiscard]] std::string trim(std::string const& str);
+
+[[nodiscard]] std::string_view ltrim(std::string_view str);
+[[nodiscard]] std::string ltrim(std::string const& str);
+
+[[nodiscard]] std::string_view rtrim(std::string_view str);
+[[nodiscard]] std::string rtrim(std::string const& str);
+
+[[nodiscard]] bool starts_with(std::string_view str, std::string_view prefix);
+[[nodiscard]] bool ends_with(std::string_view str, std::string_view suffix);
+
+} // namespace str
+
+#endif // STRUTIL_HH
diff --git a/src/tag.cc b/src/tag.cc
new file mode 100644
index 0000000..a563b9d
--- /dev/null
+++ b/src/tag.cc
@@ -0,0 +1,139 @@
+#include "common.hh"
+
+#include "htmlutil.hh"
+#include "tag.hh"
+
+#include <map>
+#include <vector>
+
+namespace {
+
+class TextRenderable : public virtual Renderable {
+public:
+ explicit TextRenderable(std::string content)
+ : content_(std::move(content)) {}
+
+ void render(std::string* out) const override {
+ html::escape(content_, out);
+ }
+
+private:
+ std::string content_;
+};
+
+class ScriptRenderable : public virtual Renderable {
+public:
+ explicit ScriptRenderable(std::string content)
+ : content_(std::move(content)) {}
+
+ void render(std::string* out) const override {
+ out->append(content_);
+ }
+
+private:
+ std::string content_;
+};
+
+class TagImpl : public Tag {
+public:
+ TagImpl(std::string name, std::string content)
+ : name_(std::move(name)) {
+ add(std::move(content));
+ assert(html::escape(name_) == name_);
+ }
+
+ std::string_view name() const override {
+ return name_;
+ }
+
+ bool empty() const override {
+ return child_.empty();
+ }
+
+ bool has_attr(std::string const& name) const override {
+ return attr_.count(name);
+ }
+
+ Tag* clear_content() override {
+ child_.clear();
+ return this;
+ }
+
+ Tag* attr(std::string name, std::string value) override {
+ assert(html::escape(name) == name);
+ attr_[name] = std::move(value);
+ return this;
+ }
+
+ Tag* add(std::string content) override {
+ if (!content.empty()) {
+ if (name_ == "script") {
+ child_.push_back(
+ std::make_unique<ScriptRenderable>(std::move(content)));
+ } else {
+ child_.push_back(std::make_unique<TextRenderable>(std::move(content)));
+ }
+ }
+ return this;
+ }
+
+ Tag* add(std::unique_ptr<Tag> tag) override {
+ auto* ret = tag.get();
+ child_.push_back(std::move(tag));
+ return ret;
+ }
+
+ void render(std::string* out) const override {
+ size_t need = 1 + name_.size();
+ for (auto const& pair : attr_)
+ need += 1 + pair.first.size() + 2 + pair.second.size() + 1;
+ if (empty())
+ ++need;
+ out->reserve(need);
+ out->push_back('<');
+ out->append(name_);
+ for (auto const& pair : attr_) {
+ out->push_back(' ');
+ out->append(pair.first);
+ out->append("=\"");
+ html::escape(pair.second, out, html::EscapeTarget::ATTRIBUTE);
+ out->push_back('"');
+ }
+ if (empty()) {
+ if (name_ == "script") {
+ // Some browsers don't allow <script src=""/>, must be written as
+ // <script src=""></script>
+ out->append("></script>");
+ } else {
+ // There are tags, like <br> where the / is optional but why botter.
+ out->append("/>");
+ }
+ return;
+ }
+ out->push_back('>');
+
+ for (auto& child : child_) {
+ child->render(out);
+ }
+
+ out->reserve(name_.size() + 3);
+ out->append("</");
+ out->append(name_);
+ out->push_back('>');
+ }
+
+private:
+ std::string name_;
+ std::map<std::string, std::string> attr_;
+ std::vector<std::unique_ptr<Renderable>> child_;
+};
+
+} // namespace
+
+Tag* Tag::add_tag(std::string name, std::string content) {
+ return add(create(std::move(name), std::move(content)));
+}
+
+std::unique_ptr<Tag> Tag::create(std::string name, std::string content) {
+ return std::make_unique<TagImpl>(std::move(name), std::move(content));
+}
diff --git a/src/tag.hh b/src/tag.hh
new file mode 100644
index 0000000..ce7f149
--- /dev/null
+++ b/src/tag.hh
@@ -0,0 +1,42 @@
+#ifndef TAG_HH
+#define TAG_HH
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+class Renderable {
+public:
+ virtual ~Renderable() = default;
+
+ virtual void render(std::string* out) const = 0;
+
+protected:
+ Renderable() = default;
+};
+
+class Tag : public virtual Renderable {
+public:
+ static std::unique_ptr<Tag> create(std::string name,
+ std::string content = std::string());
+
+ virtual std::string_view name() const = 0;
+ virtual bool empty() const = 0;
+ virtual bool has_attr(std::string const& name) const = 0;
+
+ virtual Tag* attr(std::string name, std::string value) = 0;
+ virtual Tag* add(std::string content) = 0;
+ // Does not clear attributes.
+ virtual Tag* clear_content() = 0;
+
+ // These return the new child tag, not this.
+ virtual Tag* add(std::unique_ptr<Tag> tag) = 0;
+ virtual Tag* add_tag(std::string name, std::string content = std::string());
+
+protected:
+ Tag() = default;
+ Tag(Tag const&) = delete;
+ Tag& operator=(Tag const&) = delete;
+};
+
+#endif // TAG_HH
diff --git a/src/task_runner.hh b/src/task_runner.hh
new file mode 100644
index 0000000..d1c79d6
--- /dev/null
+++ b/src/task_runner.hh
@@ -0,0 +1,24 @@
+#ifndef TASK_RUNNER_HH
+#define TASK_RUNNER_HH
+
+#include <functional>
+#include <memory>
+
+class Looper;
+
+class TaskRunner {
+public:
+ virtual ~TaskRunner() = default;
+
+ static std::unique_ptr<TaskRunner> create(std::shared_ptr<Looper> looper);
+ static std::unique_ptr<TaskRunner> create(size_t threads = 1);
+
+ virtual void post(std::function<void()> callback) = 0;
+
+protected:
+ TaskRunner() = default;
+ TaskRunner(TaskRunner const&) = delete;
+ TaskRunner& operator=(TaskRunner const&) = delete;
+};
+
+#endif // TASK_RUNNER_HH
diff --git a/src/task_runner_looper.cc b/src/task_runner_looper.cc
new file mode 100644
index 0000000..4c1292b
--- /dev/null
+++ b/src/task_runner_looper.cc
@@ -0,0 +1,75 @@
+#include "common.hh"
+
+#include <deque>
+#include <mutex>
+
+#include "io.hh"
+#include "looper.hh"
+#include "task_runner.hh"
+#include "unique_pipe.hh"
+
+namespace {
+
+constexpr char kMessage = 1;
+
+class TaskRunnerLooper : public TaskRunner {
+public:
+ explicit TaskRunnerLooper(std::shared_ptr<Looper> looper)
+ : looper_(looper), pipe_(true, false) {
+ looper_->add(pipe_.reader(), Looper::EVENT_READ,
+ std::bind(&TaskRunnerLooper::run, this,
+ std::placeholders::_1));
+ }
+
+ ~TaskRunnerLooper() override {
+ looper_->remove(pipe_.reader());
+ }
+
+ void post(std::function<void()> callback) override {
+ bool notify;
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ notify = queue_.empty();
+ queue_.push_back(std::move(callback));
+ }
+ if (notify)
+ io::write_all(pipe_.writer(), &kMessage, 1);
+ }
+
+private:
+ void run(uint8_t event) {
+ if (event & Looper::EVENT_READ) {
+ char in[1];
+ io::read_all(pipe_.reader(), &in, 1);
+ }
+ if (event & Looper::EVENT_ERROR) {
+ assert(false);
+ looper_->quit();
+ }
+ bool more;
+ do {
+ std::function<void()> callback;
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ if (queue_.empty())
+ break;
+ callback = std::move(queue_.front());
+ queue_.pop_front();
+ more = !queue_.empty();
+ }
+ callback();
+ } while (more);
+ }
+
+ std::shared_ptr<Looper> looper_;
+ unique_pipe pipe_;
+ std::mutex mutex_;
+ std::deque<std::function<void()>> queue_;
+};
+
+} // namespace
+
+std::unique_ptr<TaskRunner> TaskRunner::create(std::shared_ptr<Looper> looper) {
+ return std::make_unique<TaskRunnerLooper>(looper);
+}
+
diff --git a/src/task_runner_reply.hh b/src/task_runner_reply.hh
new file mode 100644
index 0000000..1ca65a0
--- /dev/null
+++ b/src/task_runner_reply.hh
@@ -0,0 +1,19 @@
+#ifndef TASK_RUNNER_REPLY_HH
+#define TASK_RUNNER_REPLY_HH
+
+#include "task_runner.hh"
+
+#include <utility>
+
+template<typename R>
+void post_and_reply(TaskRunner* callback_runner,
+ std::function<R()> callback,
+ std::shared_ptr<TaskRunner> reply_runner,
+ std::function<void(R)> reply) {
+ callback_runner->post([callback, reply, reply_runner] () {
+ auto r = callback();
+ reply_runner->post(std::bind(std::move(reply), std::move(r)));
+ });
+}
+
+#endif // TASK_RUNNER_REPLY_HH
diff --git a/src/task_runner_thread.cc b/src/task_runner_thread.cc
new file mode 100644
index 0000000..6e1a06f
--- /dev/null
+++ b/src/task_runner_thread.cc
@@ -0,0 +1,73 @@
+#include "common.hh"
+
+#include <algorithm>
+#include <condition_variable>
+#include <deque>
+#include <mutex>
+#include <thread>
+
+#include "task_runner.hh"
+
+namespace {
+
+class TaskRunnerThread : public TaskRunner {
+public:
+ explicit TaskRunnerThread(size_t threads)
+ : threads_(std::max<size_t>(1, threads)) {
+ thread_ = std::make_unique<std::thread[]>(threads_);
+ for (size_t i = 0; i < threads_; ++i)
+ thread_[i] = std::thread(&TaskRunnerThread::thread, this);
+ }
+
+ ~TaskRunnerThread() override {
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ quit_ = true;
+ }
+ cond_.notify_all();
+ for (size_t i = 0; i < threads_; ++i)
+ thread_[i].join();
+ }
+
+ void post(std::function<void()> callback) override {
+ {
+ std::lock_guard<std::mutex> lock(mutex_);
+ queue_.push_back(std::move(callback));
+ }
+ cond_.notify_one();
+ }
+
+private:
+ void thread() {
+ while (true) {
+ std::function<void()> callback;
+ while (true) {
+ std::unique_lock<std::mutex> lock(mutex_);
+ if (queue_.empty()) {
+ if (quit_)
+ return;
+ cond_.wait(lock);
+ } else {
+ callback = std::move(queue_.front());
+ queue_.pop_front();
+ break;
+ }
+ }
+
+ callback();
+ }
+ }
+
+ size_t const threads_;
+ bool quit_{false};
+ std::condition_variable cond_;
+ std::mutex mutex_;
+ std::deque<std::function<void()>> queue_;
+ std::unique_ptr<std::thread[]> thread_;
+};
+
+} // namespace
+
+std::unique_ptr<TaskRunner> TaskRunner::create(size_t threads) {
+ return std::make_unique<TaskRunnerThread>(threads);
+}
diff --git a/src/timezone.cc b/src/timezone.cc
new file mode 100644
index 0000000..ca0f501
--- /dev/null
+++ b/src/timezone.cc
@@ -0,0 +1,38 @@
+#include "common.hh"
+
+#include "geo_json.hh"
+#include "timezone.hh"
+#include "tz_info.hh"
+
+namespace {
+
+class TimezoneImpl : public Timezone {
+public:
+ TimezoneImpl(std::shared_ptr<Logger> logger,
+ std::filesystem::path geojsondb,
+ std::filesystem::path tzinfo_dir)
+ : geojson_(GeoJson::create(logger, std::move(geojsondb))),
+ tzinfo_(TzInfo::create(logger, std::move(tzinfo_dir))) {
+ }
+
+ std::optional<time_t> get_local_time(double lat, double lng,
+ time_t utc_time) const override {
+ auto tzid = geojson_->get_data(lat, lng, "tzid");
+ if (tzid.has_value())
+ return tzinfo_->get_local_time(tzid.value(), utc_time);
+ return std::nullopt;
+ }
+
+private:
+ std::unique_ptr<GeoJson> geojson_;
+ std::unique_ptr<TzInfo> tzinfo_;
+};
+
+} // namespace
+
+std::unique_ptr<Timezone> Timezone::create(std::shared_ptr<Logger> logger,
+ std::filesystem::path geojsondb,
+ std::filesystem::path tzinfo_dir) {
+ return std::make_unique<TimezoneImpl>(std::move(logger), std::move(geojsondb),
+ std::move(tzinfo_dir));
+}
diff --git a/src/timezone.hh b/src/timezone.hh
new file mode 100644
index 0000000..3d33eb8
--- /dev/null
+++ b/src/timezone.hh
@@ -0,0 +1,27 @@
+#ifndef TIMEZONE_HH
+#define TIMEZONE_HH
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+
+class Logger;
+
+class Timezone {
+public:
+ virtual ~Timezone() = default;
+
+ static std::unique_ptr<Timezone> create(std::shared_ptr<Logger> logger,
+ std::filesystem::path geojsondb,
+ std::filesystem::path tzinfo_dir);
+
+ virtual std::optional<time_t> get_local_time(double lat, double lng,
+ time_t utc_time) const = 0;
+
+protected:
+ Timezone() = default;
+ Timezone(Timezone const&) = delete;
+ Timezone& operator=(Timezone const&) = delete;
+};
+
+#endif // TIMEZONE_HH
diff --git a/src/transport.cc b/src/transport.cc
new file mode 100644
index 0000000..8195db7
--- /dev/null
+++ b/src/transport.cc
@@ -0,0 +1,195 @@
+#include "common.hh"
+
+#include "pathutil.hh"
+#include "strutil.hh"
+#include "transport.hh"
+
+#include <utility>
+
+namespace {
+
+class NoContentInput : public Transport::Input {
+public:
+ Return fill(Buffer*, size_t) override {
+ return Return::END;
+ }
+
+ void wait_once(std::shared_ptr<Looper>, std::function<void()> callback)
+ override {
+ assert(false);
+ callback();
+ }
+};
+
+class NoContentResponse : public Transport::Response {
+public:
+ explicit NoContentResponse(std::unique_ptr<Transport::Response> response)
+ : response_(std::move(response)) {
+ }
+
+ uint16_t code() const override {
+ return response_->code();
+ }
+
+ std::vector<std::pair<std::string, std::string>> const&
+ headers() const override {
+ return response_->headers();
+ }
+
+ std::unique_ptr<Transport::Input> open_content() override {
+ return std::make_unique<NoContentInput>();
+ }
+
+ void add_header(std::string name, std::string value) override {
+ response_->add_header(std::move(name), std::move(value));
+ }
+
+private:
+ std::unique_ptr<Transport::Response> response_;
+};
+
+void copy_headers(Transport::Response const* src, Transport::Response* dst,
+ std::string_view name) {
+ for (auto const& header_pair : src->headers()) {
+ if (header_pair.first == name) {
+ dst->add_header(std::string(header_pair.first),
+ std::string(header_pair.second));
+ }
+ }
+}
+
+std::optional<std::string_view> get_first_header(
+ Transport::Response const* resp, std::string_view name) {
+ for (auto const& header_pair : resp->headers()) {
+ if (header_pair.first == name) {
+ return header_pair.second;
+ }
+ }
+ return std::nullopt;
+}
+
+class DefaultHandler : public Transport::Handler {
+public:
+ DefaultHandler(std::shared_ptr<Logger> logger, Transport::Handler* handler)
+ : logger_(logger), handler_(handler) {}
+
+ std::unique_ptr<Transport::Response> request(
+ Transport* transport,
+ Transport::Request const* request) override {
+ bool include_content;
+ if (request->method() == "GET") {
+ include_content = true;
+ } else if (request->method() == "HEAD") {
+ include_content = false;
+ } else {
+ return transport->create_data(405, "");
+ }
+
+ auto clean_path = path::cleanup(request->path());
+ if (clean_path != request->path()) {
+ auto response = transport->create_redirect(clean_path, false);
+ return response;
+ }
+
+ auto response = handler_->request(transport, request);
+
+ if (!response)
+ return nullptr;
+
+ bool not_modified = false;
+
+ {
+ auto values = request->header_all("if-none-match");
+ auto etag = get_first_header(response.get(), "ETag");
+
+ bool match = false;
+ for (auto const& value : values) {
+ if (value == "*") {
+ match = true;
+ break;
+ }
+ if (!etag)
+ continue;
+ if (str::starts_with(value, "W/") || str::starts_with(value, "w/")) {
+ if (value.compare(2, value.size() - 2, etag.value()) == 0) {
+ match = true;
+ break;
+ }
+ } else {
+ if (value == etag.value()) {
+ match = true;
+ break;
+ }
+ }
+ }
+
+ if (match)
+ not_modified = true;
+ }
+
+ // TODO: Add If-Modified-Since? Not useful right now tho as we have
+ // no content that returns Date: headers.
+
+ if (not_modified) {
+ auto new_response = transport->create_data(304, "");
+ copy_headers(response.get(), new_response.get(), "Cache-Control");
+ copy_headers(response.get(), new_response.get(), "Content-Location");
+ copy_headers(response.get(), new_response.get(), "Date");
+ copy_headers(response.get(), new_response.get(), "ETag");
+ copy_headers(response.get(), new_response.get(), "Expires");
+ copy_headers(response.get(), new_response.get(), "Vary");
+ return new_response;
+ }
+
+ return include_content
+ ? std::move(response)
+ : std::make_unique<NoContentResponse>(std::move(response));
+ }
+
+private:
+ std::shared_ptr<Logger> logger_;
+ Transport::Handler* const handler_;
+};
+
+} // namespace
+
+void Transport::Response::open_content_async(
+ std::shared_ptr<TaskRunner>,
+ std::function<void(std::unique_ptr<Transport::Input>)> callback) {
+ assert(false);
+ callback(open_content());
+}
+
+std::unique_ptr<Transport::Response> Transport::create_ok_data(
+ std::string data) {
+ if (data.empty())
+ return create_data(204, std::move(data));
+ return create_data(200, std::move(data));
+}
+
+std::unique_ptr<Transport::Response> Transport::create_ok_file(
+ std::filesystem::path path) {
+ return create_file(200, std::move(path));
+}
+
+std::unique_ptr<Transport::Response> Transport::create_ok_exif_thumbnail(
+ std::filesystem::path path) {
+ return create_exif_thumbnail(200, std::move(path));
+}
+
+std::unique_ptr<Transport::Response> Transport::create_not_found() {
+ return create_data(404, std::string());
+}
+
+std::unique_ptr<Transport::Response> Transport::create_redirect(
+ std::string location, bool temporary) {
+ auto ret = create_data(temporary ? 302 : 301, std::string());
+ ret->add_header("Location", std::move(location));
+ return ret;
+}
+
+std::unique_ptr<Transport::Handler> Transport::create_default_handler(
+ std::shared_ptr<Logger> logger,
+ Handler* handler) {
+ return std::make_unique<DefaultHandler>(logger, handler);
+}
diff --git a/src/transport.hh b/src/transport.hh
new file mode 100644
index 0000000..5e6733f
--- /dev/null
+++ b/src/transport.hh
@@ -0,0 +1,147 @@
+#ifndef TRANSPORT_HH
+#define TRANSPORT_HH
+
+#include "unique_fd.hh"
+
+#include <filesystem>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+class Buffer;
+class Config;
+class Logger;
+class Looper;
+class TaskRunner;
+
+class Transport {
+public:
+ class Request {
+ public:
+ virtual ~Request() = default;
+
+ virtual std::string_view method() const = 0;
+ virtual std::string_view path() const = 0;
+ virtual std::string_view query(std::string_view name) const = 0;
+ virtual std::optional<std::string> header_one(
+ std::string_view name) const = 0;
+ virtual std::vector<std::string> header_all(
+ std::string_view name) const = 0;
+ };
+
+ class Input {
+ public:
+ virtual ~Input() = default;
+
+ enum class Return {
+ OK, // At least one byte was added to buffer.
+ FULL, // Zero bytes added to buffer as it is full.
+ END, // Zero or more bytes added to buffer but there are no more bytes
+ // available because input has ended.
+ ERR, // No bytes added to buffer and there was an fatal error reading
+ // any more bytes.
+ WAIT, // Zero or more bytes added to buffer but wait before calling
+ // fill again (see wait_once).
+ };
+
+ virtual Return fill(Buffer* buffer, size_t buf_request_size = 1) = 0;
+
+ // Setup callback to be called when Input is ready to be read again using
+ // looper. Callback will only be called once. Callback might be called
+ // before wait returns if Input is already ready. If Input is destroyed
+ // before getting ready the callback will never be called.
+ virtual void wait_once(std::shared_ptr<Looper> looper,
+ std::function<void()> callback) = 0;
+
+ protected:
+ Input() = default;
+ };
+
+ class Response {
+ public:
+ virtual ~Response() = default;
+
+ virtual uint16_t code() const = 0;
+ virtual std::vector<std::pair<std::string, std::string>> const&
+ headers() const = 0;
+ // Only call this once. If it returns null, call open_content_async instead.
+ virtual std::unique_ptr<Input> open_content() = 0;
+ // Only call this once and only if open_content() first returned nullptr.
+ // If response is destroyed before callback is posted the callback will
+ // never be posted. But if callback is already posted then it will run,
+ // and the input will still be valid.
+ virtual void open_content_async(
+ std::shared_ptr<TaskRunner> runner,
+ std::function<void(std::unique_ptr<Input>)> callback);
+
+ virtual void add_header(std::string name, std::string value) = 0;
+
+ protected:
+ Response() = default;
+ };
+
+ std::unique_ptr<Response> create_ok_data(std::string data);
+ std::unique_ptr<Response> create_ok_file(std::filesystem::path path);
+ std::unique_ptr<Response> create_ok_exif_thumbnail(
+ std::filesystem::path path);
+ std::unique_ptr<Response> create_not_found();
+ std::unique_ptr<Response> create_redirect(std::string target,
+ bool temporary = true);
+ virtual std::unique_ptr<Response> create_data(
+ uint16_t code, std::string data) = 0;
+ virtual std::unique_ptr<Response> create_file(
+ uint16_t code, std::filesystem::path data) = 0;
+ virtual std::unique_ptr<Response> create_exif_thumbnail(
+ uint16_t code, std::filesystem::path data) = 0;
+
+ class Handler {
+ public:
+ virtual ~Handler() = default;
+
+ virtual std::unique_ptr<Response> request(Transport* transport,
+ Request const* request) = 0;
+
+ protected:
+ Handler() = default;
+ };
+
+ // Takes care of GET/HEAD, optional cache headers and general housekeeping.
+ // Also removes // and similar from paths.
+ static std::unique_ptr<Handler> create_default_handler(
+ std::shared_ptr<Logger> logger,
+ Handler* handler);
+
+ class Factory {
+ public:
+ virtual ~Factory() = default;
+
+ virtual std::unique_ptr<Transport> create(
+ std::shared_ptr<Logger> logger,
+ std::shared_ptr<Looper> looper,
+ std::shared_ptr<TaskRunner> runner,
+ // config_logger is used to write any errors during create
+ // when reading the config. No reference kept as with logger.
+ Logger* config_logger,
+ Config const* config,
+ Handler* handler) = 0;
+
+ protected:
+ Factory() = default;
+ Factory(Factory const&) = delete;
+ Factory& operator=(Factory const&) = delete;
+ };
+
+ virtual ~Transport() = default;
+
+ virtual void add_client(unique_fd&& fd) = 0;
+
+protected:
+ Transport() = default;
+ Transport(Transport const&) = delete;
+ Transport& operator=(Transport const&) = delete;
+};
+
+#endif // TRANSPORT_HH
diff --git a/src/transport_base.cc b/src/transport_base.cc
new file mode 100644
index 0000000..00d582b
--- /dev/null
+++ b/src/transport_base.cc
@@ -0,0 +1,677 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "file_opener.hh"
+#include "image.hh"
+#include "io.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "task_runner.hh"
+#include "transport_base.hh"
+#include "urlutil.hh"
+
+namespace {
+
+class ResponseImpl : public Transport::Response {
+public:
+ uint16_t code() const override {
+ return code_;
+ }
+
+ std::vector<std::pair<std::string, std::string>> const& headers()
+ const override {
+ return headers_;
+ }
+
+ void add_header(std::string name, std::string value) override {
+ headers_.emplace_back(std::move(name), std::move(value));
+ }
+
+protected:
+ explicit ResponseImpl(uint16_t code)
+ : code_(code) {}
+
+private:
+ uint16_t const code_;
+ std::vector<std::pair<std::string, std::string>> headers_;
+};
+
+class DataInput : public Transport::Input {
+public:
+ explicit DataInput(std::string data)
+ : data_(std::move(data)) {}
+
+ Return fill(Buffer* buffer, size_t) override {
+ if (offset_ >= data_.size())
+ return Return::END;
+ auto bytes = Buffer::write(buffer,data_.data() + offset_,
+ data_.size() - offset_);
+ if (bytes > 0) {
+ offset_ += bytes;
+ return Return::OK;
+ }
+ return Return::FULL;
+ }
+
+ void wait_once(std::shared_ptr<Looper>,
+ std::function<void()> callback) override {
+ assert(false);
+ callback();
+ }
+
+private:
+ std::string data_;
+ size_t offset_{0};
+};
+
+class ResponseData : public ResponseImpl {
+public:
+ ResponseData(uint16_t code, std::string data)
+ : ResponseImpl(code), data_(std::move(data)) {
+ add_header("Content-Length", std::to_string(data_.size()));
+ }
+
+ std::unique_ptr<Transport::Input> open_content() override {
+ return std::make_unique<DataInput>(std::move(data_));
+ }
+
+private:
+ std::string data_;
+};
+
+class FileInput : public Transport::Input {
+public:
+ explicit FileInput(unique_fd&& fd)
+ : fd_(std::move(fd)) {}
+
+ ~FileInput() override {
+ if (looper_)
+ looper_->remove(fd_.get());
+ }
+
+ Return fill(Buffer* buffer, size_t buf_request_size) override {
+ size_t bytes;
+ switch (io::fill(fd_.get(), buffer, buf_request_size, &bytes)) {
+ case io::Return::OK:
+ break;
+ case io::Return::ERR:
+ return Return::ERR;
+ case io::Return::CLOSED:
+ return Return::END;
+ }
+
+ if (bytes > 0)
+ return Return::OK;
+ if (buffer->full())
+ return Return::FULL;
+ return Return::WAIT;
+ }
+
+ void wait_once(std::shared_ptr<Looper> looper,
+ std::function<void()> callback) override {
+ if (looper_) {
+ assert(false);
+ looper_->remove(fd_.get());
+ }
+ looper_ = looper;
+ waiting_callback_ = std::move(callback);
+ looper_->add(fd_.get(), Looper::EVENT_READ,
+ std::bind(&FileInput::event, this, std::placeholders::_1));
+ }
+
+private:
+ void event(uint8_t) {
+ looper_->remove(fd_.get());
+ looper_.reset();
+ auto callback = std::move(waiting_callback_);
+ callback();
+ }
+
+ unique_fd fd_;
+ std::shared_ptr<Looper> looper_;
+ std::function<void()> waiting_callback_;
+};
+
+class ErrorInput : public Transport::Input {
+public:
+ Return fill(Buffer*, size_t) override {
+ return Return::ERR;
+ }
+
+ void wait_once(std::shared_ptr<Looper>,
+ std::function<void()> callback) override {
+ assert(false);
+ callback();
+ }
+};
+
+class ResponseFile : public ResponseImpl {
+public:
+ ResponseFile(uint16_t code, std::shared_ptr<FileOpener> file_opener,
+ std::filesystem::path path)
+ : ResponseImpl(code), opener_(std::move(file_opener)),
+ open_id_(opener_->open(std::move(path),
+ std::bind(&ResponseFile::opened, this,
+ std::placeholders::_1,
+ std::placeholders::_2))) {
+ }
+
+ ~ResponseFile() {
+ if (open_id_)
+ opener_->cancel(open_id_);
+ }
+
+ std::unique_ptr<Transport::Input> open_content() override {
+ if (open_id_)
+ return nullptr;
+ if (fd_)
+ return create_input(std::move(fd_));
+ return std::make_unique<ErrorInput>();
+ }
+
+ void open_content_async(
+ std::shared_ptr<TaskRunner> runner,
+ std::function<void(std::unique_ptr<Transport::Input>)> callback)
+ override {
+ if (open_id_) {
+ waiting_ = std::make_unique<WaitingCallback>(std::move(callback));
+ waiting_runner_ = std::move(runner);
+ } else {
+ callback(open_content());
+ }
+ }
+
+protected:
+ virtual std::unique_ptr<Transport::Input> create_input(unique_fd&& fd) {
+ return std::make_unique<FileInput>(std::move(fd));
+ }
+
+private:
+ class WaitingCallback {
+ public:
+ explicit WaitingCallback(
+ std::function<void(std::unique_ptr<Transport::Input>)> callback)
+ : callback_(std::move(callback)) {}
+
+ void input(std::unique_ptr<Transport::Input> input) {
+ assert(!input_);
+ input_ = std::move(input);
+ }
+
+ void call() {
+ assert(input_);
+ callback_(std::move(input_));
+ }
+
+ private:
+ std::function<void(std::unique_ptr<Transport::Input>)> callback_;
+ std::unique_ptr<Transport::Input> input_;
+ };
+
+ void opened(uint32_t id, unique_fd fd) {
+ assert(open_id_ == id);
+ open_id_ = 0;
+ opener_.reset();
+ fd_ = std::move(fd);
+
+ if (waiting_) {
+ waiting_->input(open_content());
+ waiting_runner_->post(std::bind(&WaitingCallback::call,
+ waiting_));
+ waiting_.reset();
+ waiting_runner_.reset();
+ }
+ }
+
+ std::shared_ptr<FileOpener> opener_;
+ uint32_t open_id_;
+ unique_fd fd_;
+ std::shared_ptr<WaitingCallback> waiting_;
+ std::shared_ptr<TaskRunner> waiting_runner_;
+};
+
+class ExifThumbnailInput : public FileInput {
+public:
+ explicit ExifThumbnailInput(unique_fd&& fd)
+ : FileInput(std::move(fd)), reader_(ThumbnailReader::create()) {}
+
+ Return fill(Buffer* buffer, size_t buf_request_size) override {
+ if (buf_) {
+ auto file_ret = FileInput::fill(buf_.get(), 1);
+ switch (reader_->drain(buf_.get())) {
+ case ThumbnailReader::Return::NEED_MORE:
+ break;
+ case ThumbnailReader::Return::DONE:
+ buf_.reset();
+ return fill_with_data(buffer, buf_request_size);
+ case ThumbnailReader::Return::ERR:
+ return Return::ERR;
+ }
+ switch (file_ret) {
+ case Return::OK:
+ case Return::ERR:
+ case Return::WAIT:
+ return file_ret;
+ case Return::FULL:
+ // ThumbnailReader should drain more than this
+ assert(false);
+ return Return::ERR;
+ case Return::END:
+ return Return::ERR;
+ }
+ } else {
+ return fill_with_data(buffer, buf_request_size);
+ }
+ }
+
+private:
+ Return fill_with_data(Buffer* buffer, size_t buf_request_size) {
+ if (offset_ >= reader_->data().size())
+ return Return::END;
+ size_t avail;
+ auto* ptr = buffer->wbuf(buf_request_size, avail);
+ if (avail == 0)
+ return Return::FULL;
+ auto got = reader_->data().size() - offset_;
+ if (avail > got)
+ avail = got;
+ std::copy_n(reader_->data().data() + offset_, avail, ptr);
+ buffer->wcommit(avail);
+ offset_ += avail;
+ return Return::OK;
+ }
+
+ std::unique_ptr<ThumbnailReader> reader_;
+ std::unique_ptr<Buffer> buf_{Buffer::fixed(10 * 1024)};
+ size_t offset_{0};
+};
+
+class ResponseExifThumbnail : public ResponseFile {
+public:
+ ResponseExifThumbnail(uint16_t code, std::shared_ptr<FileOpener> file_opener,
+ std::filesystem::path path)
+ : ResponseFile(code, std::move(file_opener), std::move(path)) {}
+
+protected:
+ std::unique_ptr<Transport::Input> create_input(unique_fd&& fd) override {
+ return std::make_unique<ExifThumbnailInput>(std::move(fd));
+ }
+};
+
+} // namespace
+
+TransportBase::TransportBase(std::shared_ptr<Logger> logger,
+ std::shared_ptr<Looper> looper,
+ std::shared_ptr<TaskRunner> runner,
+ Handler* handler)
+ : logger_(logger), looper_(looper), runner_(runner), handler_(handler) {
+}
+
+TransportBase::~TransportBase() {
+ // Clear these before calling client_abort to not cause any
+ // unnecessary client_new.
+ client_wait_.clear();
+
+ for (auto& client : client_)
+ client_abort(&client);
+}
+
+uint64_t TransportBase::default_client_input_buffer_size() const {
+ // No POST/PUT support, really shouldn't be that big.
+ return 100 * 1024;
+}
+
+uint64_t TransportBase::default_client_output_buffer_size() const {
+ // Might return actual files, but that is async so whole file
+ // doesn't need to fit.
+ return 1 * 1024 * 1024;
+}
+
+std::unique_ptr<Transport::Response> TransportBase::create_data(
+ uint16_t code, std::string data) {
+ return std::make_unique<ResponseData>(code, std::move(data));
+}
+
+std::unique_ptr<Transport::Response> TransportBase::create_file(
+ uint16_t code, std::filesystem::path path) {
+ return std::make_unique<ResponseFile>(code, file_opener_, std::move(path));
+}
+
+std::unique_ptr<Transport::Response> TransportBase::create_exif_thumbnail(
+ uint16_t code, std::filesystem::path path) {
+ return std::make_unique<ResponseExifThumbnail>(code, file_opener_,
+ std::move(path));
+}
+
+bool TransportBase::setup(Logger* logger, Config const* config) {
+ client_.clear();
+ auto clients = config->get("transport.max_clients", 10);
+ if (!clients.has_value()) {
+ logger->err("transport.max_clients is unknown value: '%s'",
+ config->get("transport.max_clients", nullptr));
+ return false;
+ }
+ if (clients.value() < 1) {
+ logger->err("transport.max_clients must be > 0");
+ return false;
+ }
+ for (size_t i = 0; i < clients.value(); ++i)
+ client_.emplace_back(i);
+ auto in_buffer_size = config->get_size("client.input_buffer_size",
+ default_client_input_buffer_size());
+ if (!in_buffer_size.has_value()) {
+ logger->err("client.input_buffer_size is unknown size: `%s'",
+ config->get("client.input_buffer_size", nullptr));
+ return false;
+ }
+ if (in_buffer_size.value() < 1) {
+ logger->err("client.input_buffer_size must be > 0");
+ return false;
+ }
+ auto out_buffer_size = config->get_size("client.output_buffer_size",
+ default_client_output_buffer_size());
+ if (!out_buffer_size.has_value()) {
+ logger->err("client.output_buffer_size is unknown size: `%s'",
+ config->get("client.output_buffer_size", nullptr));
+ return false;
+ }
+ if (out_buffer_size.value() < 1) {
+ logger->err("client.output_buffer_size must be > 0");
+ return false;
+ }
+ for (auto& client : client_) {
+ client.in_ = Buffer::fixed(in_buffer_size.value());
+ client.out_ = Buffer::fixed(out_buffer_size.value());
+ }
+ auto timeout = config->get_duration("client.timeout", 30.0);
+ if (!timeout.has_value()) {
+ logger->err("client.timeout is unknown duration: `%s'",
+ config->get("client.timeout", nullptr));
+ return false;
+ }
+ if (timeout.value() <= 0.0) {
+ logger->err("client.timeout must be > 0");
+ return false;
+ }
+ client_timeout_ = timeout.value();
+ auto file_opener_threads = config->get("transport.workers", 1);
+ if (!file_opener_threads.has_value()) {
+ logger->err("transport.workers is unknown value: '%s'",
+ config->get("transport.workers", nullptr));
+ return false;
+ }
+ if (file_opener_threads.value() <= 0) {
+ logger->err("transport.workers must be > 0");
+ return false;
+ }
+ file_opener_ = FileOpener::create(runner_, file_opener_threads.value());
+ return true;
+}
+
+void TransportBase::add_client(unique_fd&& fd) {
+ if (!client_full_) {
+ auto const start = next_avail_client_;
+ do {
+ auto& client = client_[next_avail_client_++];
+ if (next_avail_client_ == client_.size())
+ next_avail_client_ = 0;
+ if (!client.fd_) {
+ client.fd_ = std::move(fd);
+ client_new(&client);
+
+ // Assume there is data available directly to speed up responses
+ // in the common case.
+ client_event(&client, Looper::EVENT_READ);
+ return;
+ }
+ } while (next_avail_client_ != start);
+ client_full_ = true;
+ }
+ client_wait_.push_back(std::move(fd));
+}
+
+void TransportBase::client_new(Client* client) {
+ assert(client->fd_);
+ looper_->add(client->fd_.get(), Looper::EVENT_READ,
+ std::bind(&TransportBase::client_event, this, client,
+ std::placeholders::_1));
+ client->last_event_ = std::chrono::steady_clock::now();
+ client->timeout_ = looper_->schedule(
+ client_timeout_,
+ std::bind(&TransportBase::client_timeout, this, client,
+ std::placeholders::_1));
+}
+
+void TransportBase::client_timeout(Client* client, uint32_t id) {
+ assert(client->timeout_ == id);
+ client->timeout_ = 0;
+
+ std::chrono::duration<double> delay =
+ std::chrono::steady_clock::now() - client->last_event_;
+
+ if (delay.count() < client_timeout_) {
+ client->timeout_ = looper_->schedule(
+ client_timeout_ - delay.count(),
+ std::bind(&TransportBase::client_timeout, this, client,
+ std::placeholders::_1));
+ } else {
+ logger_->dbg("Client timeout %zu", client->index_);
+ client_abort(client);
+ }
+}
+
+void TransportBase::client_event(Client* client, uint8_t event) {
+ client->last_event_ = std::chrono::steady_clock::now();
+
+ bool call_handle = false;
+
+ if (event & Looper::EVENT_READ) {
+ size_t bytes = 0;
+ switch (io::fill(client->fd_.get(), client->in_.get(),
+ client->expect_in_, &bytes)) {
+ case io::Return::OK:
+ if (bytes > 0)
+ call_handle = true;
+ break;
+ case io::Return::ERR:
+ logger_->dbg("Error reading from client %zu", client->index_);
+ client_abort(client);
+ return;
+ case io::Return::CLOSED:
+ if (!client->in_closed_)
+ call_handle = true;
+ client->in_closed_ = true;
+ break;
+ }
+ }
+ if (event & Looper::EVENT_WRITE) {
+ size_t bytes = 0;
+ if (!io::drain(client->out_.get(), client->fd_.get(), &bytes)) {
+ logger_->dbg("Error writing to client %zu", client->index_);
+ client_abort(client);
+ return;
+ }
+ if (bytes > 0)
+ call_handle = true;
+ }
+ if (event & Looper::EVENT_ERROR) {
+ logger_->dbg("Looper error on client %zu", client->index_);
+ client_abort(client);
+ return;
+ }
+
+ if (call_handle) {
+ if (!client_handle(client)) {
+ client_abort(client);
+ return;
+ }
+ }
+
+ client_update_event(client);
+}
+
+void TransportBase::client_update_event(Client* client) {
+ uint8_t events = 0;
+ if (!client->in_closed_ && client->expect_in_ > 0 && !client->in_->full())
+ events |= Looper::EVENT_READ;
+ if (!client->out_->empty())
+ events |= Looper::EVENT_WRITE;
+ looper_->update(client->fd_.get(), events);
+}
+
+bool TransportBase::client_flush(Client* client) {
+ size_t bytes;
+ const bool was_full = client->out_->full();
+ if (io::drain(client->out_.get(), client->fd_.get(), &bytes)) {
+ if (bytes > 0) {
+ client->last_event_ = std::chrono::steady_clock::now();
+ }
+
+ if (!client->out_->empty()) {
+ // Make sure to add EVENT_WRITE
+ client_update_event(client);
+ }
+
+ if (bytes > 0 && was_full) {
+ for (auto& pair : client->responses_) {
+ if (pair.second.response_ && pair.second.content_) {
+ if (!client_response_content(client, pair.first))
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ logger_->dbg("Error writing to client %zu", client->index_);
+ client_abort(client);
+ return false;
+}
+
+void TransportBase::client_abort(Client* client) {
+ if (!client->fd_)
+ return;
+
+ looper_->remove(client->fd_.get());
+ if (client->timeout_) {
+ looper_->cancel(client->timeout_);
+ client->timeout_ = 0;
+ }
+ client->fd_.reset();
+ client->in_->clear();
+ client->out_->clear();
+ client->responses_.clear();
+ client->in_closed_ = false;
+ client->expect_in_ = 1;
+
+ next_avail_client_ = client->index_;
+ client_full_ = false;
+}
+
+bool TransportBase::client_response(Client* client, uint32_t id,
+ std::unique_ptr<Response> response) {
+ auto ret = client->responses_.emplace(id, std::move(response));
+ assert(ret.second);
+ if (!client_response_header(client, id))
+ return false;
+ assert(client->responses_.count(id));
+ auto& cli_response = client->responses_[id];
+ cli_response.content_ = cli_response.response_->open_content();
+ if (cli_response.content_)
+ return client_response_content(client, id);
+
+ cli_response.response_->open_content_async(
+ runner_, std::bind(&TransportBase::client_response_open, this, client,
+ id, std::placeholders::_1));
+ return true;
+}
+
+void TransportBase::client_response_open(Client* client, uint32_t id,
+ std::unique_ptr<Input> input) {
+ assert(client->responses_.count(id));
+ auto& cli_response = client->responses_[id];
+ cli_response.content_ = std::move(input);
+ client_response_content(client, id);
+}
+
+bool TransportBase::client_response_header(Client*, uint32_t) {
+ return true;
+}
+
+bool TransportBase::client_response_content(Client* client, uint32_t id) {
+ return client_response_content(client, id, client->out_.get());
+}
+
+bool TransportBase::client_response_content(Client* client, uint32_t id,
+ Buffer* out) {
+ assert(client->responses_.count(id));
+ auto& cli_response = client->responses_[id];
+ switch (cli_response.content_->fill(out)) {
+ case Input::Return::OK:
+ if (!client_flush(client))
+ return false;
+ return client->responses_.count(id) == 0 ||
+ client_response_content(client, id);
+ case Input::Return::FULL:
+ return client_flush(client);
+ case Input::Return::END:
+ return client_response_end(client, id);
+ case Input::Return::ERR:
+ logger_->warn("Input error for client %zu", client->index_);
+ client_abort(client);
+ return false;
+ case Input::Return::WAIT:
+ cli_response.content_->wait_once(
+ looper_,
+ std::bind(&TransportBase::client_response_content_wait, this,
+ client, id));
+ return true;
+ }
+ assert(false);
+ return true;
+}
+
+void TransportBase::client_response_content_wait(Client* client, uint32_t id) {
+ client_response_content(client, id);
+}
+
+bool TransportBase::client_response_end(Client* client, uint32_t id) {
+ if (!client_response_footer(client, id))
+ return false;
+ client->responses_.erase(id);
+ if (!client_handle(client))
+ return false;
+ client_update_event(client);
+ return true;
+}
+
+bool TransportBase::client_response_footer(Client*, uint32_t) {
+ return true;
+}
+
+bool TransportBase::client_request(Client* client, uint32_t id,
+ std::unique_ptr<Request> request) {
+ auto response = handler_->request(this, request.get());
+ if (response) {
+ return client_response(client, id, std::move(response));
+ } else {
+ return client_response(client, id, create_data(500, ""));
+ }
+}
+
+std::string_view TransportBase::UrlRequest::path() const {
+ split_url_if_needed();
+ return path_;
+}
+
+std::string_view TransportBase::UrlRequest::query(std::string_view name) const {
+ split_url_if_needed();
+ auto it = query_.find(std::string(name));
+ if (it == query_.end())
+ return std::string_view();
+ return it->second;
+}
+
+void TransportBase::UrlRequest::split_url_if_needed() const {
+ if (path_.empty())
+ url::split_and_unescape_path_and_query(url(), path_, query_);
+}
diff --git a/src/transport_base.hh b/src/transport_base.hh
new file mode 100644
index 0000000..61d7601
--- /dev/null
+++ b/src/transport_base.hh
@@ -0,0 +1,113 @@
+#ifndef TRANSPORT_BASE_HH
+#define TRANSPORT_BASE_HH
+
+#include "buffer.hh"
+#include "transport.hh"
+
+#include <chrono>
+#include <deque>
+
+class FileOpener;
+
+class TransportBase : public Transport {
+public:
+ ~TransportBase() override;
+
+ std::unique_ptr<Response> create_data(
+ uint16_t code, std::string data) override;
+
+ std::unique_ptr<Response> create_file(
+ uint16_t code, std::filesystem::path data) override;
+
+ std::unique_ptr<Response> create_exif_thumbnail(
+ uint16_t code, std::filesystem::path data) override;
+
+ void add_client(unique_fd&& fd) override;
+
+ virtual bool setup(Logger* logger, Config const* config);
+
+protected:
+ struct ClientResponse {
+ std::unique_ptr<Response> response_;
+ std::unique_ptr<Input> content_;
+
+ ClientResponse() = default;
+ explicit ClientResponse(std::unique_ptr<Response> response)
+ : response_(std::move(response)) {}
+ };
+
+ struct Client {
+ size_t const index_;
+ unique_fd fd_;
+ std::unique_ptr<Buffer> in_;
+ std::unique_ptr<Buffer> out_;
+ std::unordered_map<uint32_t, ClientResponse> responses_;
+
+ bool in_closed_{false};
+ size_t expect_in_{1};
+ uint32_t timeout_{0};
+ std::chrono::steady_clock::time_point last_event_;
+
+ explicit Client(size_t index)
+ : index_(index) {}
+ };
+
+ class UrlRequest : public Request {
+ public:
+ std::string_view path() const override;
+ std::string_view query(std::string_view name) const override;
+
+ protected:
+ virtual std::string_view url() const = 0;
+
+ private:
+ void split_url_if_needed() const;
+
+ std::string mutable path_;
+ std::unordered_map<std::string, std::string> mutable query_;
+ };
+
+ TransportBase(std::shared_ptr<Logger> logger, std::shared_ptr<Looper> looper,
+ std::shared_ptr<TaskRunner> runner, Handler* handler);
+
+ virtual uint64_t default_client_input_buffer_size() const;
+ virtual uint64_t default_client_output_buffer_size() const;
+
+ virtual void client_new(Client* client);
+ virtual void client_event(Client* client, uint8_t event);
+ virtual void client_abort(Client* client);
+ virtual bool client_flush(Client* client);
+ virtual bool client_handle(Client* client) = 0;
+ virtual void client_timeout(Client* client, uint32_t id);
+ virtual bool client_response(Client* client, uint32_t id,
+ std::unique_ptr<Response> response);
+ virtual bool client_response_header(Client* client, uint32_t id);
+ virtual bool client_response_content(Client* client, uint32_t id);
+ virtual bool client_response_footer(Client* client, uint32_t id);
+ virtual bool client_response_end(Client* client, uint32_t id);
+ void client_update_event(Client* client);
+ bool client_request(Client* client, uint32_t id,
+ std::unique_ptr<Request> request);
+ bool client_response_content(Client* client, uint32_t id, Buffer* out);
+
+ size_t clients() const { return client_.size(); }
+
+ std::shared_ptr<Logger> logger_;
+ std::shared_ptr<Looper> looper_;
+ std::shared_ptr<TaskRunner> runner_;
+ Handler* const handler_;
+ std::shared_ptr<FileOpener> file_opener_;
+
+private:
+ void client_response_content_wait(Client* client, uint32_t response_id);
+ void client_response_open(Client* client, uint32_t response_id,
+ std::unique_ptr<Input> input);
+
+ std::vector<Client> client_;
+ bool client_full_{false};
+ size_t next_avail_client_{0};
+ std::vector<unique_fd> client_wait_;
+ double client_timeout_;
+};
+
+#endif // TRANSPORT_BASE_HH
diff --git a/src/transport_fastcgi.cc b/src/transport_fastcgi.cc
new file mode 100644
index 0000000..8a5c3cc
--- /dev/null
+++ b/src/transport_fastcgi.cc
@@ -0,0 +1,707 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "fcgi_protocol.hh"
+#include "http_protocol.hh"
+#include "logger.hh"
+#include "strutil.hh"
+#include "transport_base.hh"
+#include "transport_fastcgi.hh"
+
+#include <unordered_map>
+#include <vector>
+
+namespace {
+
+class FastCgiTransport : public TransportBase {
+public:
+ FastCgiTransport(std::shared_ptr<Logger> logger,
+ std::shared_ptr<Looper> looper,
+ std::shared_ptr<TaskRunner> runner,
+ Transport::Handler* handler)
+ : TransportBase(logger, looper, runner, handler) {
+ }
+
+ bool setup(Logger* logger, Config const* config) override {
+ if (!TransportBase::setup(logger, config))
+ return false;
+
+ auto max_requests = config->get("transport.fcgi.max_requests", 20);
+ if (!max_requests.has_value()) {
+ logger->err("transport.fcgi.max_requests is unknown value: '%s'",
+ config->get("transport.fcgi.max_requests", nullptr));
+ return false;
+ }
+ if (max_requests.value() < 1) {
+ logger->err("transport.fcgi.max_requests must be > 0");
+ return false;
+ }
+
+ max_requests_ = max_requests.value();
+
+ extra_.resize(clients());
+ for (auto& extra : extra_) {
+ extra.requests.resize(max_requests_);
+ }
+
+ return true;
+ }
+
+ bool client_handle(Client* client) override {
+ auto& extra = extra_[client->index_];
+
+ if (extra.close_connection) {
+ client->expect_in_ = 0;
+ if (client->out_->empty()) {
+ client_abort(client);
+ return false;
+ }
+ return true;
+ }
+
+ // A fastcgi records can be quite small, handle as many as possible
+ // before returning.
+ while (true) {
+ if (!extra.record) {
+ extra.record = fcgi::Record::parse(client->in_.get());
+ if (!extra.record) {
+ // Need more input
+ size_t avail;
+ client->in_->rbuf(8, avail);
+ assert(avail < 8); // if avail >= 8 parse should not return nullptr
+ client->expect_in_ = avail >= 8 ? 1 : 8 - avail;
+ if (client->in_closed_) {
+ if (client->out_->empty()) {
+ client_abort(client);
+ return false;
+ }
+ // Wait for output to be sent (or timeout, whichever is first).
+ }
+ return true;
+ }
+ if (!extra.record->good()) {
+ logger_->warn("Bad record sent by client: %zu", client->index_);
+ client_abort(client);
+ return false;
+ }
+ extra.content_offset = 0;
+ }
+
+ if (extra.record->request_id() != 0 &&
+ extra.record->type() != fcgi::RecordType::BeginRequest &&
+ extra.request_map.count(extra.record->request_id()) == 0) {
+ logger_->dbg("Ignoring message for unknown request for client: %zu",
+ client->index_);
+ // Ignoring messages for unknown requests
+ if (!client_consume_content(client))
+ return false;
+ } else {
+ switch (extra.record->type()) {
+ case fcgi::RecordType::GetValues:
+ if (extra.record->request_id() == 0) {
+ if (!client_handle_get_values(client))
+ return false;
+ } else {
+ if (!client_handle_unknown(client))
+ return false;
+ }
+ break;
+ case fcgi::RecordType::GetValuesResult:
+ case fcgi::RecordType::EndRequest:
+ case fcgi::RecordType::Stdout:
+ case fcgi::RecordType::Stderr:
+ logger_->warn("Client %zu sending application record type",
+ client->index_);
+ client_abort(client);
+ return false;
+ case fcgi::RecordType::BeginRequest:
+ if (extra.record->request_id() == 0) {
+ logger_->warn("Client %zu sending BeginRequest for 0",
+ client->index_);
+ client_abort(client);
+ return false;
+ }
+ if (!client_handle_begin_request(client))
+ return false;
+ break;
+ case fcgi::RecordType::AbortRequest:
+ if (extra.record->request_id() == 0) {
+ logger_->warn("Client %zu sending AbortRequest for 0",
+ client->index_);
+ client_abort(client);
+ return false;
+ }
+ if (!client_handle_abort_request(client))
+ return false;
+ break;
+ case fcgi::RecordType::Params:
+ if (extra.record->request_id() == 0) {
+ logger_->warn("Client %zu sending Params for 0", client->index_);
+ client_abort(client);
+ return false;
+ }
+ if (!client_handle_params(client))
+ return false;
+ break;
+ case fcgi::RecordType::Stdin:
+ if (extra.record->request_id() == 0) {
+ logger_->warn("Client %zu sending Stdin for 0", client->index_);
+ client_abort(client);
+ return false;
+ }
+ if (!client_handle_stdin(client))
+ return false;
+ break;
+ default:
+ if (!client_handle_unknown(client))
+ return false;
+ }
+ }
+
+ if (extra.record) {
+ // Need more data
+ return true;
+ }
+ }
+ }
+
+ void client_new(Client* client) override {
+ TransportBase::client_new(client);
+
+ auto& extra = extra_[client->index_];
+ extra.request_map.clear();
+ extra.record.reset();
+ extra.stream.reset();
+ extra.pair.reset();
+ extra.content_offset = 0;
+ extra.values.clear();
+ extra.close_connection = false;
+
+ for (auto& request : extra.requests) {
+ request.active = false;
+ request.stream.reset();
+ request.pair.reset();
+ request.params.clear();
+ }
+ }
+
+private:
+ class StdoutBuffer : public Buffer {
+ public:
+ void use(uint32_t request_id, Buffer* out) {
+ assert(!ptr_);
+ id_ = request_id;
+ out_ = out;
+ good_ = true;
+ }
+
+ bool good() const {
+ return good_;
+ }
+
+ bool full() const override {
+ assert(out_);
+ return out_->full();
+ }
+
+ void clear() override {
+ assert(false);
+ }
+
+ char* wbuf(size_t request, size_t& avail) override {
+ assert(!ptr_);
+ assert(out_);
+ auto ptr = out_->wbuf(request + 16, avail);
+ if (avail >= 16) {
+ avail -= 16;
+ if (avail > std::numeric_limits<uint16_t>::max())
+ avail = std::numeric_limits<uint16_t>::max();
+ ptr_ = ptr;
+ return ptr + 8;
+ } else {
+ avail = 0;
+ ptr_ = nullptr;
+ return nullptr;
+ }
+ }
+
+ void wcommit(size_t bytes) override {
+ assert(out_);
+ if (bytes > 0) {
+ assert(ptr_);
+ assert(bytes <= std::numeric_limits<uint16_t>::max());
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Stdout,
+ id_, bytes);
+ bool result = builder->build(ptr_, 8) &&
+ builder->padding(ptr_ + 8 + bytes, 8);
+ if (!result)
+ good_ = false;
+ out_->wcommit(builder->size());
+ }
+ ptr_ = nullptr;
+ }
+
+ bool empty() const override {
+ assert(out_);
+ return out_->empty();
+ }
+
+ char const* rbuf(size_t, size_t& avail) override {
+ assert(false);
+ avail = 0;
+ return nullptr;
+ }
+
+ void rcommit(size_t) override {
+ assert(false);
+ }
+
+ private:
+ uint32_t id_ = 0;
+ Buffer* out_ = nullptr;
+ char* ptr_ = nullptr;
+ bool good_ = true;
+ };
+
+ class FastCgiRequest : public UrlRequest {
+ public:
+ explicit FastCgiRequest(std::vector<std::pair<std::string, std::string>>
+ params)
+ : params_(std::move(params)) {
+ }
+
+ std::string_view method() const override {
+ return find("REQUEST_METHOD", "GET");
+ }
+
+ std::string_view url() const override {
+ return find("REQUEST_URI", "/");
+ }
+
+ std::optional<std::string> header_one(
+ std::string_view name) const override {
+ for (auto const& pair : params_) {
+ if (lower_case_equal(pair.first, name))
+ return pair.second;
+ }
+ return std::nullopt;
+ }
+
+ std::vector<std::string> header_all(std::string_view name) const override {
+ std::vector<std::string> ret;
+ for (auto const& pair : params_) {
+ if (lower_case_equal(pair.first, name)) {
+ auto tmp = str::split(pair.second, ',');
+ for (auto str : tmp)
+ ret.push_back(std::string(str::trim(str)));
+ }
+ }
+ return ret;
+ }
+
+ private:
+ static bool lower_case_equal(std::string_view a, std::string_view b) {
+ if (a.size() != b.size())
+ return false;
+ for (size_t i = 0; i < a.size(); ++i) {
+ if (lower_case(a[i]) != lower_case(b[i]))
+ return false;
+ }
+ return true;
+ }
+
+ static bool lower_case(char c) {
+ return (c >= 'A' && c <= 'Z') ? (c | 0x20) : c;
+ }
+
+ std::string_view find(std::string_view name,
+ std::string_view fallback) const {
+ for (auto const& pair : params_) {
+ if (pair.first == name)
+ return pair.second;
+ }
+ return fallback;
+ }
+
+ std::vector<std::pair<std::string, std::string>> params_;
+ };
+
+ struct Request {
+ bool keep_conn{false};
+ bool active{false};
+ std::unique_ptr<fcgi::RecordStream> stream;
+ std::unique_ptr<fcgi::Pair> pair;
+ bool add_to_stream;
+ std::vector<std::pair<std::string, std::string>> params;
+ std::unique_ptr<fcgi::RecordStream> stdin;
+ bool add_to_stdin;
+ std::optional<uint32_t> app_state;
+ };
+
+ struct Extra {
+ std::unordered_map<uint16_t, size_t> request_map;
+ std::vector<Request> requests;
+ std::unique_ptr<fcgi::Record> record;
+ std::unique_ptr<fcgi::RecordStream> stream;
+ std::unique_ptr<fcgi::Pair> pair;
+ size_t content_offset;
+ std::vector<std::string> values;
+ bool close_connection;
+ };
+
+ bool client_handle_unknown(Client* client) {
+ auto& extra = extra_[client->index_];
+ if (extra.record->request_id() == 0) {
+ if (extra.content_offset == 0)
+ logger_->dbg("Client %zu got unknown maintainence record.",
+ client->index_);
+ auto type = extra.record->type();
+ if (!client_consume_content(client))
+ return false;
+ if (extra.record)
+ return true;
+ auto builder = fcgi::RecordBuilder::create_unknown_type(type);
+ return client_send(client, std::move(builder));
+ } else {
+ if (extra.content_offset == 0)
+ logger_->info("Client %zu got unknown record type for request: %u",
+ client->index_, extra.record->type());
+ return client_consume_content(client);
+ }
+ }
+
+ bool client_send(Client* client,
+ std::unique_ptr<fcgi::RecordBuilder> builder) {
+ if (!builder->build(client->out_.get())) {
+ logger_->warn("Client output buffer full: %zu", client->index_);
+ client_abort(client);
+ return false;
+ }
+ return client_flush(client);
+ }
+
+ bool client_handle_get_values(Client* client) {
+ auto& extra = extra_[client->index_];
+ assert(extra.record);
+ assert(extra.record->type() == fcgi::RecordType::GetValues);
+ bool need_more;
+ if (!extra.stream)
+ extra.stream = fcgi::RecordStream::create_single(extra.record.get());
+ if (!extra.pair) {
+ extra.pair = fcgi::Pair::start(extra.stream.get(), client->in_.get());
+ need_more = !extra.pair;
+ } else {
+ need_more = !extra.pair->next(extra.stream.get(), client->in_.get());
+ }
+ while (!need_more) {
+ if (!extra.pair->good()) {
+ logger_->warn("Client %zu sent invalid GetValues", client->index_);
+ client_abort(client);
+ return false;
+ }
+ extra.values.push_back(extra.pair->name());
+ need_more = !extra.pair->next(extra.stream.get(), client->in_.get());
+ }
+ if (extra.stream->end_of_stream())
+ return client_handle_end_of_get_values(client);
+ // Need more data
+ return true;
+ }
+
+ bool client_handle_end_of_get_values(Client* client) {
+ auto& extra = extra_[client->index_];
+ extra.pair.reset();
+ extra.stream.reset();
+ extra.record.reset();
+ auto pair_builder = fcgi::PairBuilder::create();
+ for (auto& value : extra.values) {
+ if (value == "FCGI_MAX_CONNS") {
+ pair_builder->add(std::move(value), std::to_string(clients()));
+ } else if (value == "FCGI_MAX_REQS") {
+ pair_builder->add(std::move(value), std::to_string(max_requests_));
+ } else if (value == "FCGI_MPXS_CONNS") {
+ pair_builder->add(std::move(value), "1");
+ } else {
+ logger_->dbg("Unknown value `%s` ignored by client: %zu",
+ value.c_str(), client->index_);
+ }
+ }
+ extra.values.clear();
+ auto builder =
+ fcgi::RecordBuilder::create(fcgi::RecordType::GetValuesResult,
+ 0,
+ pair_builder->size());
+ if (!builder->build(client->out_.get()) ||
+ !pair_builder->build(client->out_.get()) ||
+ !builder->padding(client->out_.get())) {
+ logger_->warn("Client output buffer full: %zu", client->index_);
+ client_abort(client);
+ return false;
+ }
+ return client_flush(client);
+ }
+
+ bool client_handle_begin_request(Client* client) {
+ auto& extra = extra_[client->index_];
+ assert(extra.record);
+ assert(extra.record->type() == fcgi::RecordType::BeginRequest);
+ auto body = fcgi::BeginRequestBody::parse(extra.record.get(),
+ client->in_.get());
+ if (!body)
+ return true; // Need more data
+ if (!body->good()) {
+ logger_->warn("Client sent invalid begin request: %zu", client->index_);
+ client_abort(client);
+ return false;
+ }
+ auto request_id = extra.record->request_id();
+ extra.record.reset();
+ if (body->role() != fcgi::Role::Responder) {
+ return client_send(client,
+ fcgi::RecordBuilder::create_end_request(
+ request_id, 1,
+ fcgi::ProtocolStatus::UnknownRole));
+ }
+ size_t i = 0;
+ for (; i < max_requests_; ++i)
+ if (!extra.requests[i].active)
+ break;
+ if (i == max_requests_) {
+ return client_send(client,
+ fcgi::RecordBuilder::create_end_request(
+ request_id, 1,
+ fcgi::ProtocolStatus::Overloaded));
+ }
+ auto ret = extra.request_map.emplace(request_id, i);
+ if (!ret.second) {
+ logger_->warn("Client sent double begin request: %zu", client->index_);
+ client_abort(client);
+ return false;
+ }
+ extra.requests[i].active = true;
+ extra.requests[i].keep_conn = body->flags() & fcgi::Flags::KeepConn;
+ return true;
+ }
+
+ bool client_handle_abort_request(Client* client) {
+ auto& extra = extra_[client->index_];
+ assert(extra.record);
+ assert(extra.record->type() == fcgi::RecordType::AbortRequest);
+ auto request_id = extra.record->request_id();
+ if (!client_consume_content(client))
+ return false;
+ if (extra.record)
+ return true; // Need more data
+ return client_end_request(client, request_id, 1);
+ }
+
+ bool client_end_request(Client* client, uint16_t request_id,
+ uint32_t app_state) {
+ auto& extra = extra_[client->index_];
+ auto it = extra.request_map.find(request_id);
+ if (it == extra.request_map.end()) {
+ assert(false);
+ return true;
+ }
+ auto& request = extra.requests[it->second];
+ request.params.clear();
+ if (!request.stdin || !request.stdin->end_of_stream()) {
+ request.app_state = app_state;
+ return true;
+ }
+ auto keep_conn = request.keep_conn;
+ request.active = false;
+ request.app_state.reset();
+ request.stream.reset();
+ request.stdin.reset();
+ request.pair.reset();
+ extra.request_map.erase(it);
+ if (!client_send(client,
+ fcgi::RecordBuilder::create_end_request(
+ request_id, app_state,
+ fcgi::ProtocolStatus::RequestComplete)))
+ return false;
+ if (keep_conn)
+ return true;
+
+ // TODO: Should this check that no other requests are active?
+ extra.close_connection = true;
+ client->expect_in_ = 0;
+ if (client->out_->empty()) {
+ client_abort(client);
+ return false;
+ }
+ return true;
+ }
+
+ bool client_handle_params(Client* client) {
+ auto& extra = extra_[client->index_];
+ assert(extra.record);
+ assert(extra.record->type() == fcgi::RecordType::Params);
+ auto& request = extra.requests.at(
+ extra.request_map.at(extra.record->request_id()));
+ if (!request.stream) {
+ request.stream = fcgi::RecordStream::create_stream(extra.record.get());
+ request.add_to_stream = false;
+ } else {
+ if (request.add_to_stream) {
+ request.stream->add(extra.record.get());
+ request.add_to_stream = false;
+ }
+ }
+ bool need_more;
+ if (!request.pair) {
+ request.pair = fcgi::Pair::start(request.stream.get(), client->in_.get());
+ need_more = !request.pair;
+ } else {
+ need_more = !request.pair->next(request.stream.get(), client->in_.get());
+ }
+ while (!need_more) {
+ if (!request.pair->good()) {
+ logger_->warn("Client %zu sending invalid params", client->index_);
+ client_abort(client);
+ return false;
+ }
+ request.params.emplace_back(request.pair->name(), request.pair->value());
+ need_more = !request.pair->next(request.stream.get(), client->in_.get());
+ }
+ if (request.stream->end_of_stream()) {
+ auto id = extra.record->request_id();
+ extra.record.reset();
+ request.stream.reset();
+ request.pair.reset();
+ return TransportBase::client_request(
+ client, id,
+ std::make_unique<FastCgiRequest>(std::move(request.params)));
+ }
+ if (request.stream->end_of_record()) {
+ extra.record.reset();
+ request.add_to_stream = true;
+ }
+ return true;
+ }
+
+ bool client_handle_stdin(Client* client) {
+ auto& extra = extra_[client->index_];
+ assert(extra.record);
+ assert(extra.record->type() == fcgi::RecordType::Stdin);
+ auto& request = extra.requests.at(
+ extra.request_map.at(extra.record->request_id()));
+ if (!request.stdin) {
+ request.stdin = fcgi::RecordStream::create_stream(extra.record.get());
+ request.add_to_stdin = false;
+ } else {
+ if (request.add_to_stdin) {
+ request.stdin->add(extra.record.get());
+ request.add_to_stdin = false;
+ }
+ }
+ // Just consume the stdin stream data, it isn't used for anything
+ while (true) {
+ size_t avail;
+ request.stdin->rbuf(client->in_.get(), 1, avail);
+ if (avail == 0)
+ break;
+ request.stdin->rcommit(client->in_.get(), avail);
+ }
+ if (request.stdin->end_of_stream()) {
+ auto id = extra.record->request_id();
+ extra.record.reset();
+ if (request.app_state)
+ return client_end_request(client, id, *request.app_state);
+ } else if (request.stdin->end_of_record()) {
+ extra.record.reset();
+ request.add_to_stdin = true;
+ }
+ return true;
+ }
+
+ bool client_consume_content(Client* client) {
+ auto& extra = extra_[client->index_];
+ assert(extra.record);
+ size_t content_size = static_cast<size_t>(extra.record->content_length()) +
+ extra.record->padding_length();
+ if (extra.content_offset >= content_size) {
+ extra.record.reset();
+ return true;
+ }
+ size_t need = content_size - extra.content_offset;
+ size_t avail;
+ client->in_->rbuf(need, avail);
+ if (avail >= need) {
+ client->in_->rcommit(need);
+ extra.record.reset();
+ return true;
+ }
+ extra.content_offset += avail;
+ client->in_->rcommit(avail);
+ return true;
+ }
+
+ bool client_response_header(Client* client, uint32_t id) override {
+ assert(client->responses_.count(id));
+ auto& cli_response = client->responses_[id];
+ auto content_builder =
+ CgiResponseBuilder::create(cli_response.response_->code());
+ for (auto const& pair : cli_response.response_->headers())
+ content_builder->add_header(pair.first, pair.second);
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Stdout,
+ id, content_builder->size());
+ if (!builder->build(client->out_.get()) ||
+ !content_builder->build(client->out_.get()) ||
+ !builder->padding(client->out_.get())) {
+ logger_->warn("Output buffer full for client: %zu", client->index_);
+ client_abort(client);
+ return false;
+ }
+ return client_flush(client);
+ }
+
+ bool client_response_content(Client* client, uint32_t id) override {
+ stdout_.use(id, client->out_.get());
+ if (!TransportBase::client_response_content(client, id, &stdout_))
+ return false;
+ return stdout_.good();
+ }
+
+ bool client_response_footer(Client* client, uint32_t id) override {
+ assert(client->responses_.count(id));
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Stdout, id,
+ std::string());
+ if (!builder->build(client->out_.get())) {
+ logger_->warn("Output buffer full for client: %zu", client->index_);
+ client_abort(client);
+ return false;
+ }
+ if (!client_flush(client))
+ return false;
+ return client_end_request(client, id, 0);
+ }
+
+ std::vector<Extra> extra_;
+ size_t max_requests_{0};
+ StdoutBuffer stdout_;
+};
+
+class FastCgiFactory : public Transport::Factory {
+public:
+ std::unique_ptr<Transport> create(
+ std::shared_ptr<Logger> logger,
+ std::shared_ptr<Looper> looper,
+ std::shared_ptr<TaskRunner> runner,
+ Logger* config_logger,
+ Config const* config,
+ Transport::Handler* handler) {
+ auto transport =
+ std::make_unique<FastCgiTransport>(logger, looper, runner, handler);
+ if (transport->setup(config_logger, config))
+ return transport;
+ return nullptr;
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Transport::Factory> create_transport_factory_fastcgi() {
+ return std::make_unique<FastCgiFactory>();
+}
diff --git a/src/transport_fastcgi.hh b/src/transport_fastcgi.hh
new file mode 100644
index 0000000..d574bd5
--- /dev/null
+++ b/src/transport_fastcgi.hh
@@ -0,0 +1,8 @@
+#ifndef TRANSPORT_FASTCGI_HH
+#define TRANSPORT_FASTCGI_HH
+
+#include "transport.hh"
+
+std::unique_ptr<Transport::Factory> create_transport_factory_fastcgi();
+
+#endif // TRANSPORT_FASTCGI_HH
diff --git a/src/transport_http.cc b/src/transport_http.cc
new file mode 100644
index 0000000..98e7fed
--- /dev/null
+++ b/src/transport_http.cc
@@ -0,0 +1,193 @@
+#include "common.hh"
+
+#include "http_protocol.hh"
+#include "logger.hh"
+#include "strutil.hh"
+#include "transport_base.hh"
+#include "transport_http.hh"
+
+namespace {
+
+class HttpTransport : public TransportBase {
+public:
+ HttpTransport(std::shared_ptr<Logger> logger,
+ std::shared_ptr<Looper> looper,
+ std::shared_ptr<TaskRunner> runner,
+ Transport::Handler* handler)
+ : TransportBase(logger, looper, runner, handler) {
+ }
+
+ bool setup(Logger* logger, Config const* config) override {
+ if (!TransportBase::setup(logger, config))
+ return false;
+
+ extra_.resize(clients());
+ return true;
+ }
+
+ bool client_handle(Client* client) override {
+ auto& extra = extra_[client->index_];
+ auto req = HttpRequest::parse(client->in_.get());
+ if (!req) {
+ client->expect_in_ = 1; // Don't know how big the request will be
+ if (client->in_closed_ || extra.close_connection_) {
+ if (client->out_->empty()) {
+ client_abort(client);
+ return false;
+ }
+ // Wait for output to be sent (or timeout, whichever is first).
+ }
+ return true;
+ }
+
+ if (req->good()) {
+ if (supported_request(req.get())) {
+ extra.close_connection_ = close_connection(req.get());
+ extra.version_ = req->proto_version();
+ // Stop reading in_ buffer until request is handled.
+ client->expect_in_ = 0;
+ return TransportBase::client_request(
+ client, 0, std::make_unique<WrapRequest>(std::move(req)));
+ } else {
+ return client_fatal_response(client, 505);
+ }
+ } else {
+ return client_fatal_response(client, 400);
+ }
+ }
+
+ static bool supported_request(HttpRequest const* request) {
+ if (request->proto() != "HTTP")
+ return false;
+ auto version = request->proto_version();
+ return version.major == 1;
+ }
+
+ static bool close_connection(HttpRequest const* request) {
+ if (request->proto_version().major == 1 &&
+ request->proto_version().minor == 1) {
+ return request->first_header("connection") == "close";
+ }
+ return true;
+ }
+
+ void client_new(Client* client) override {
+ TransportBase::client_new(client);
+
+ auto& extra = extra_[client->index_];
+ extra.close_connection_ = true;
+ extra.version_.major = 1;
+ extra.version_.minor = 0;
+ }
+
+ bool client_fatal_response(Client* client, uint16_t status_code) {
+ auto& extra = extra_[client->index_];
+ extra.close_connection_ = true;
+ client->expect_in_ = 0;
+ client->in_->clear();
+ return client_response(client, 0, create_data(status_code, ""));
+ }
+
+ bool client_response_header(Client* client, uint32_t id) override {
+ auto& extra = extra_[client->index_];
+ assert(client->responses_.count(id));
+ auto& cli_response = client->responses_[id];
+ auto status_code = cli_response.response_->code();
+ auto builder = HttpResponseBuilder::create(
+ "HTTP", extra.version_,
+ status_code,
+ std::string(http_standard_message(status_code)));
+ bool have_content_length = false;
+ for (auto const& pair : cli_response.response_->headers()) {
+ if (!have_content_length && pair.first == "Content-Length")
+ have_content_length = true;
+ builder->add_header(pair.first, pair.second);
+ }
+ if (!have_content_length)
+ extra.close_connection_ = true;
+ if (extra.close_connection_)
+ builder->add_header("Connection", "close");
+ if (!builder->build(client->out_.get())) {
+ logger_->warn("Output buffer full for client: %zu", client->index_);
+ client_abort(client);
+ return false;
+ }
+ return client_flush(client);
+ }
+
+ bool client_response_footer(Client* client, uint32_t) override {
+ auto const& extra = extra_[client->index_];
+ if (extra.close_connection_ && client->out_->empty()) {
+ client_abort(client);
+ return false;
+ }
+ return true;
+ }
+
+private:
+ class WrapRequest : public UrlRequest {
+ public:
+ explicit WrapRequest(std::unique_ptr<HttpRequest> req)
+ : req_(std::move(req)) {}
+
+ std::string_view method() const override {
+ return req_->method();
+ }
+
+ std::string_view url() const override {
+ return req_->url();
+ }
+
+ std::optional<std::string> header_one(
+ std::string_view name) const override {
+ auto it = req_->header(name);
+ if (it->valid())
+ return it->value();
+ return std::nullopt;
+ }
+
+ std::vector<std::string> header_all(
+ std::string_view name) const override {
+ std::vector<std::string> ret;
+ for (auto it = req_->header(name); it->valid(); it->next()) {
+ auto tmp = str::split(it->value(), ',');
+ for (auto str : tmp)
+ ret.push_back(std::string(str::trim(str)));
+ }
+ return ret;
+ }
+
+ private:
+ std::unique_ptr<HttpRequest> req_;
+ };
+
+ struct Extra {
+ bool close_connection_;
+ Version version_;
+ };
+
+ std::vector<Extra> extra_;
+};
+
+class HttpFactory : public Transport::Factory {
+public:
+ std::unique_ptr<Transport> create(
+ std::shared_ptr<Logger> logger,
+ std::shared_ptr<Looper> looper,
+ std::shared_ptr<TaskRunner> runner,
+ Logger* config_logger,
+ Config const* config,
+ Transport::Handler* handler) {
+ auto transport = std::make_unique<HttpTransport>(
+ logger, looper, runner, handler);
+ if (transport->setup(config_logger, config))
+ return transport;
+ return nullptr;
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Transport::Factory> create_transport_factory_http() {
+ return std::make_unique<HttpFactory>();
+}
diff --git a/src/transport_http.hh b/src/transport_http.hh
new file mode 100644
index 0000000..386a67b
--- /dev/null
+++ b/src/transport_http.hh
@@ -0,0 +1,8 @@
+#ifndef TRANSPORT_HTTP_HH
+#define TRANSPORT_HTTP_HH
+
+#include "transport.hh"
+
+std::unique_ptr<Transport::Factory> create_transport_factory_http();
+
+#endif // TRANSPORT_HTTP_HH
diff --git a/src/travel.cc b/src/travel.cc
new file mode 100644
index 0000000..f8adf50
--- /dev/null
+++ b/src/travel.cc
@@ -0,0 +1,556 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "files_finder.hh"
+#include "image.hh"
+#include "logger.hh"
+#include "rotation.hh"
+#include "strutil.hh"
+#include "task_runner.hh"
+#include "timezone.hh"
+#include "travel.hh"
+#include "video.hh"
+#include "weak_ptr.hh"
+
+#include <algorithm>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TravelImpl : public Travel, public FilesFinder::Delegate {
+public:
+ TravelImpl(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner)
+ : logger_(std::move(logger)), runner_(std::move(runner)),
+ weak_ptr_owner_(this) {}
+
+ bool setup(Logger* logger, Config* config) override {
+ root_ = config->get_path("site.root", "");
+ if (root_.empty()) {
+ logger->err("site.root must be set to the root directory for travels.");
+ return false;
+ }
+ auto travel_files_threads = config->get("workers", 4);
+ if (!travel_files_threads.has_value()) {
+ logger->err("workers is unknown value: '%s'",
+ config->get("workers", nullptr));
+ return false;
+ }
+ if (travel_files_threads.value() <= 0) {
+ logger->err("workers must be > 0");
+ return false;
+ }
+ timezone_ = Timezone::create(logger_,
+ config->get_path("geojson.database", ""),
+ config->get_path("zoneinfo.root",
+ "/usr/share/zoneinfo"));
+ worker_threads_ = travel_files_threads.value();
+ return true;
+ }
+
+ void start() override {
+ workers_ = TaskRunner::create(worker_threads_);
+ finder_ = FilesFinder::create(logger_, workers_, root_, this,
+ worker_threads_);
+ }
+
+ void reload() override {
+ // Make implementation simpler by ignoring calls to reload() while
+ // previoys start() or reload() is still in progress.
+ if (finder_) {
+ logger_->warn("Reload called while still loading, ignored.");
+ return;
+ }
+
+ workers_.reset();
+ trips_.clear();
+ trip_index_.clear();
+
+ start();
+ }
+
+ size_t trips() const override {
+ return trips_.size();
+ }
+
+ Trip const& trip(size_t i) const override {
+ assert(i < trips_.size());
+ return trips_[i];
+ }
+
+ void call_when_loaded(std::function<void()> callback) override {
+ if (finder_) {
+ call_when_loaded_.push_back(std::move(callback));
+ } else {
+ callback();
+ }
+ }
+
+ // Called on any thread.
+ bool include_dir(std::string_view name, uint16_t depth) const override {
+ if (depth > 0)
+ return FilesFinder::Delegate::include_dir(name, depth);
+ return valid_trip_id(name);
+ }
+
+ // Runs on workers
+ void file(std::filesystem::path path) override {
+ std::string media_id;
+ std::string trip_id = get_trip_id(path, &media_id);
+ if (trip_id.empty()) {
+ logger_->warn("Ignoring %s because no trip id found", path.c_str());
+ return;
+ }
+ auto image = get_image_info(media_id, path);
+ if (image) {
+ if (image.value().date_.empty()) {
+ logger_->dbg("Ignoring %s, image without a date.", path.c_str());
+ return;
+ }
+ runner_->post(std::bind(&TravelImpl::weak_finder_image,
+ weak_ptr_owner_.get(),
+ trip_id,
+ image.value()));
+ return;
+ }
+ auto video = get_video_info(media_id, path, timezone_.get());
+ if (video) {
+ if (video.value().date_.empty()) {
+ logger_->dbg("Ignoring %s, video without a date.", path.c_str());
+ return;
+ }
+ runner_->post(std::bind(&TravelImpl::weak_finder_video,
+ weak_ptr_owner_.get(),
+ trip_id,
+ video.value()));
+ return;
+ }
+ logger_->dbg("Ignoring %s, not an image or a video.", path.c_str());
+ }
+
+ // Runs on workers
+ void done() override {
+ runner_->post(std::bind(&TravelImpl::weak_finder_done,
+ weak_ptr_owner_.get()));
+ }
+
+private:
+ struct Info {
+ std::string id_;
+ std::filesystem::path path_;
+ uint64_t width_;
+ uint64_t height_;
+ Location location_;
+ Date date_;
+
+ Info(std::string id, std::filesystem::path path,
+ uint64_t width, uint64_t height, Location location, Date date)
+ : id_(std::move(id)), path_(std::move(path)),
+ width_(width), height_(height),
+ location_(location), date_(date) {}
+ };
+
+ struct ImageInfo : Info {
+ Rotation rotation_;
+ std::string thumbnail_mime_type_;
+ uint64_t thumbnail_size_;
+
+ ImageInfo(std::string id, std::filesystem::path path,
+ uint64_t width, uint64_t height,
+ Location location, Date date, Rotation rotation,
+ std::string thumbnail_mime_type, uint64_t thumbnail_size)
+ : Info(std::move(id), std::move(path), width, height, location,
+ date), rotation_(rotation),
+ thumbnail_mime_type_(std::move(thumbnail_mime_type)),
+ thumbnail_size_(thumbnail_size) {}
+ };
+
+ struct VideoInfo : Info {
+ double length_;
+
+ VideoInfo(std::string id, std::filesystem::path path,
+ uint64_t width, uint64_t height,
+ Location location, Date date, double length)
+ : Info(std::move(id), std::move(path), width, height, location,
+ date), length_(length) {}
+ };
+
+ class MediaImpl : public virtual Media, public virtual Thumbnail {
+ public:
+ explicit MediaImpl(ImageInfo info)
+ : image_(std::move(info)) {}
+
+ explicit MediaImpl(VideoInfo info)
+ : video_(std::move(info)) {}
+
+ std::string_view id() const override {
+ return image_ ? image_->id_ : video_->id_;
+ }
+
+ std::filesystem::path const& path() const override {
+ return image_ ? image_->path_ : video_->path_;
+ }
+
+ Type type() const override {
+ return image_ ? Type::IMAGE : Type::VIDEO;
+ }
+
+ uint64_t width() const override {
+ return image_ ? image_->width_ : video_->width_;
+ }
+ uint64_t height() const override {
+ return image_ ? image_->height_ : video_->height_;
+ }
+
+ Location location() const override {
+ return image_ ? image_->location_ : video_->location_;
+ }
+
+ Date date() const override {
+ return image_ ? image_->date_ : video_->date_;
+ }
+
+ double length() const override {
+ return image_ ? 0.0 : video_->length_;
+ }
+
+ Rotation rotation() const override {
+ return image_ ? image_->rotation_ : Rotation::UNKNOWN;
+ }
+
+ Thumbnail const* thumbnail() const override {
+ return image_ && !image_->thumbnail_mime_type_.empty() &&
+ image_->thumbnail_size_ > 0 ? this : nullptr;
+ }
+
+ ThumbType thumb_type() const override {
+ return ThumbType::EXIF;
+ }
+
+ std::string_view mime_type() const override {
+ return image_->thumbnail_mime_type_;
+ }
+
+ uint64_t size() const override {
+ return image_->thumbnail_size_;
+ }
+
+ private:
+ std::optional<ImageInfo> image_;
+ std::optional<VideoInfo> video_;
+ };
+
+ class DayImpl : public Day {
+ public:
+ DayImpl(Date day, size_t first)
+ : day_(day), first_(first), last_(first) {
+ }
+
+ Date date() const override {
+ return day_;
+ }
+
+ size_t first() const override {
+ return first_;
+ }
+
+ size_t last() const override {
+ return last_;
+ }
+
+ void increment_last() {
+ ++last_;
+ }
+
+ private:
+ Date const day_;
+ size_t const first_;
+ size_t last_;
+ };
+
+ class TripImpl : public Trip {
+ public:
+ TripImpl(std::string id, std::string name, uint16_t year)
+ : id_(std::move(id)), name_(std::move(name)), year_(year) {}
+
+ std::string_view id() const override {
+ return id_;
+ }
+
+ std::string_view title() const override {
+ return name_;
+ }
+
+ uint16_t year() const override {
+ return year_;
+ }
+
+ Location location() const override {
+ return location_;
+ }
+
+ uint64_t images() const override {
+ return images_;
+ }
+
+ uint64_t videos() const override {
+ return videos_;
+ }
+
+ size_t media_count() const override {
+ return media_.size();
+ }
+
+ Media const& media(size_t i) const override {
+ assert(i < media_.size());
+ return media_[i];
+ }
+
+ size_t day_count() const override {
+ return day_.size();
+ }
+
+ Day const& day(size_t i) const override {
+ assert(i < day_.size());
+ return day_[i];
+ }
+
+ void add_image(ImageInfo info) {
+ ++images_;
+ media_.emplace_back(std::move(info));
+ }
+
+ void add_video(VideoInfo info) {
+ ++videos_;
+ media_.emplace_back(std::move(info));
+ }
+
+ void sort_media() {
+ std::sort(media_.begin(), media_.end(),
+ [] (MediaImpl const& a, MediaImpl const& b) {
+ return a.date() < b.date();
+ });
+ }
+
+ void setup_days() {
+ for (size_t i = 0; i < media_.size(); ++i) {
+ auto day = media_[i].date().day();
+ if (day_.empty() || day != day_.back().date()) {
+ assert(day_.empty() || day > day_.back().date());
+ day_.emplace_back(day, i);
+ } else {
+ assert(i == day_.back().last() + 1);
+ day_.back().increment_last();
+ }
+ }
+ }
+
+ void set_location(Location location) {
+ location_ = location;
+ }
+
+ private:
+ std::string id_;
+ std::string name_;
+ uint16_t year_;
+ Location location_;
+ uint64_t images_{0};
+ uint64_t videos_{0};
+ std::vector<MediaImpl> media_;
+ std::vector<DayImpl> day_;
+ };
+
+ // Called from workers
+ std::string get_trip_id(std::filesystem::path const& path,
+ std::string* out_child_id) const {
+ std::string child_id;
+ std::filesystem::path tmp = path;
+ while (true) {
+ if (!tmp.has_parent_path())
+ break;
+ auto parent = tmp.parent_path();
+ if (parent == root_) {
+ if (out_child_id)
+ *out_child_id = std::move(child_id);
+ return tmp.filename();
+ }
+ if (!child_id.empty())
+ child_id.insert(0, "/");
+ child_id.insert(0, tmp.filename());
+ tmp = parent;
+ }
+ return std::string();
+ }
+
+ static void weak_finder_image(std::shared_ptr<WeakPtr<TravelImpl>> weak_ptr,
+ std::string const& trip_id,
+ ImageInfo const& info) {
+ auto* ptr = weak_ptr->get();
+ if (ptr)
+ ptr->finder_image(trip_id, info);
+ }
+
+ static void weak_finder_video(std::shared_ptr<WeakPtr<TravelImpl>> weak_ptr,
+ std::string const& trip_id,
+ VideoInfo const& info) {
+ auto* ptr = weak_ptr->get();
+ if (ptr)
+ ptr->finder_video(trip_id, info);
+ }
+
+ static void weak_finder_done(std::shared_ptr<WeakPtr<TravelImpl>> weak_ptr) {
+ auto* ptr = weak_ptr->get();
+ if (ptr)
+ ptr->finder_done();
+ }
+
+ TripImpl* get_trip(std::string const& trip_id) {
+ auto it = trip_index_.find(trip_id);
+ if (it != trip_index_.end())
+ return &trips_[it->second];
+
+ std::string name;
+ uint16_t year;
+ if (parse_trip_id(trip_id, &name, &year)) {
+ auto index = trips_.size();
+ trips_.emplace_back(trip_id, std::move(name), year);
+ trip_index_[trip_id] = index;
+ return &trips_[index];
+ } else {
+ // include_dir should make sure of this doesn't happen.
+ assert(false);
+ return nullptr;
+ }
+ }
+
+ void finder_image(std::string const& trip_id,
+ ImageInfo const& info) {
+ auto* trip_ptr = get_trip(trip_id);
+ if (trip_ptr)
+ trip_ptr->add_image(info);
+ }
+
+ void finder_video(std::string const& trip_id,
+ VideoInfo const& info) {
+ auto* trip_ptr = get_trip(trip_id);
+ if (trip_ptr)
+ trip_ptr->add_video(info);
+ }
+
+ void finder_done() {
+ finder_.reset();
+
+ for (auto& trip_impl : trips_)
+ cleanup_trip(trip_impl);
+
+ logger_->info("Trips all loaded.");
+
+ for (auto& callback : call_when_loaded_) {
+ callback();
+ }
+ call_when_loaded_.clear();
+ }
+
+ void cleanup_trip(TripImpl& trip_impl) {
+ // Sort by date
+ trip_impl.sort_media();
+
+ trip_impl.setup_days();
+
+ // TODO: Remove outliers
+ // TODO: Use some weighted median instead of average
+ Location loc;
+ size_t count = 0;
+ for (size_t i = 0; i < trip_impl.media_count(); ++i) {
+ auto& media = trip_impl.media(i);
+ if (media.location().empty())
+ continue;
+ if (loc.empty()) {
+ loc = media.location();
+ count = 1;
+ } else {
+ loc.lat += media.location().lat;
+ loc.lng += media.location().lng;
+ ++count;
+ }
+ }
+ loc.lat /= count;
+ loc.lng /= count;
+ trip_impl.set_location(loc);
+ }
+
+ static std::optional<ImageInfo> get_image_info(std::string id,
+ std::filesystem::path path) {
+ auto image = Image::load(path);
+ if (image) {
+ auto* thumb = image->thumbnail();
+ return ImageInfo(std::move(id), std::move(path),
+ image->width(), image->height(),
+ image->location(), image->date(), image->rotation(),
+ thumb ? std::string(thumb->mime_type()) : std::string(),
+ thumb ? thumb->size() : 0);
+ }
+ return std::nullopt;
+ }
+
+ static std::optional<VideoInfo> get_video_info(std::string id,
+ std::filesystem::path path,
+ Timezone const* timezone) {
+ auto video = Video::load(path, timezone);
+ if (video)
+ return VideoInfo(std::move(id), std::move(path),
+ video->width(), video->height(),
+ video->location(), video->date(), video->length());
+ return std::nullopt;
+ }
+
+ static bool valid_trip_id(std::string_view id) {
+ return parse_trip_id(id, nullptr, nullptr);
+ }
+
+ static bool parse_trip_id(std::string_view id,
+ std::string* name, uint16_t* year) {
+ auto it = id.find('-');
+ if (it == std::string_view::npos)
+ return false;
+ if (name)
+ name->assign(str::trim(id.substr(0, it)));
+ auto tmp = str::parse_uint16(std::string(id, it + 1, std::string::npos));
+ if (!tmp)
+ return false;
+ if (*tmp < 1950)
+ return false;
+ if (year)
+ *year = *tmp;
+ return true;
+ }
+
+ std::shared_ptr<Logger> logger_;
+ std::shared_ptr<TaskRunner> runner_;
+ std::unique_ptr<Timezone> timezone_;
+ size_t worker_threads_{0};
+ std::filesystem::path root_;
+ std::vector<TripImpl> trips_;
+ std::unordered_map<std::string, size_t> trip_index_;
+ std::vector<std::function<void()>> call_when_loaded_;
+
+ std::mutex worker_mutex_;
+
+ // It is important that workers_ is (next to) last as it blocks leftover
+ // workers in destructor so should be destroyed first.
+ std::shared_ptr<TaskRunner> workers_;
+ // finder depends on workers so must be destroyed before.
+ std::unique_ptr<FilesFinder> finder_;
+ WeakPtrOwner<TravelImpl> weak_ptr_owner_;
+};
+
+} // namespace
+
+std::unique_ptr<Travel> Travel::create(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner) {
+ return std::make_unique<TravelImpl>(std::move(logger), std::move(runner));
+}
+
diff --git a/src/travel.hh b/src/travel.hh
new file mode 100644
index 0000000..c9cc891
--- /dev/null
+++ b/src/travel.hh
@@ -0,0 +1,137 @@
+#ifndef TRAVEL_HH
+#define TRAVEL_HH
+
+#include "date.hh"
+#include "location.hh"
+#include "rotation.hh"
+
+#include <filesystem>
+#include <functional>
+#include <memory>
+#include <string_view>
+
+class Config;
+class Logger;
+class TaskRunner;
+
+class Travel {
+public:
+ class Thumbnail {
+ public:
+ enum class ThumbType {
+ FILE,
+ EXIF,
+ };
+
+ virtual ~Thumbnail() = default;
+
+ virtual std::filesystem::path const& path() const = 0;
+
+ virtual ThumbType thumb_type() const = 0;
+
+ virtual std::string_view mime_type() const = 0;
+
+ virtual uint64_t size() const = 0;
+
+ protected:
+ Thumbnail() = default;
+ };
+
+ class Media {
+ public:
+ enum class Type {
+ IMAGE,
+ VIDEO,
+ };
+
+ virtual ~Media() = default;
+
+ virtual std::string_view id() const = 0;
+
+ virtual std::filesystem::path const& path() const = 0;
+
+ virtual Type type() const = 0;
+
+ virtual uint64_t width() const = 0;
+ virtual uint64_t height() const = 0;
+
+ virtual Location location() const = 0;
+
+ virtual Date date() const = 0;
+
+ // Only ever > 0.0 for videos
+ virtual double length() const = 0;
+
+ // Only ever != UNKNOWN for images
+ virtual Rotation rotation() const = 0;
+
+ // Returns nullptr if no thumbnail
+ virtual Thumbnail const* thumbnail() const = 0;
+
+ protected:
+ Media() = default;
+ };
+
+ class Day {
+ public:
+ virtual ~Day() = default;
+
+ // Returns 00:00:00 of the day in question.
+ virtual Date date() const = 0;
+
+ // Index in media array for first media for this day.
+ virtual size_t first() const = 0;
+
+ // Index in media array for the last media for this day.
+ // Days can't be empty, if there is just one media of the day then
+ // first() == last().
+ virtual size_t last() const = 0;
+
+ protected:
+ Day() = default;
+ };
+
+ class Trip {
+ public:
+ virtual ~Trip() = default;
+
+ virtual std::string_view id() const = 0;
+ virtual std::string_view title() const = 0;
+ virtual uint16_t year() const = 0;
+ virtual Location location() const = 0;
+ virtual uint64_t images() const = 0;
+ virtual uint64_t videos() const = 0;
+
+ virtual size_t media_count() const = 0;
+ virtual Media const& media(size_t i) const = 0;
+
+ virtual size_t day_count() const = 0;
+ virtual Day const& day(size_t i) const = 0;
+
+ protected:
+ Trip() = default;
+ };
+
+ virtual ~Travel() = default;
+
+ static std::unique_ptr<Travel> create(std::shared_ptr<Logger> logger,
+ std::shared_ptr<TaskRunner> runner);
+
+ virtual bool setup(Logger* logger, Config* config) = 0;
+
+ virtual void start() = 0;
+
+ virtual void reload() = 0;
+
+ virtual void call_when_loaded(std::function<void()> callback) = 0;
+
+ virtual size_t trips() const = 0;
+ virtual Trip const& trip(size_t i) const = 0;
+
+protected:
+ Travel() = default;
+ Travel(Travel const&) = delete;
+ Travel& operator=(Travel const&) = delete;
+};
+
+#endif // TRAVEL_HH
diff --git a/src/tz_info.cc b/src/tz_info.cc
new file mode 100644
index 0000000..7c3361e
--- /dev/null
+++ b/src/tz_info.cc
@@ -0,0 +1,329 @@
+#include "common.hh"
+
+#include "io.hh"
+#include "logger.hh"
+#include "tz_info.hh"
+#include "tz_str.hh"
+
+#include <byteswap.h>
+#include <errno.h>
+#include <string.h>
+#include <vector>
+#include <unistd.h>
+
+namespace {
+
+struct Header {
+ char magic[4];
+ char ver;
+ char unused[15];
+ uint32_t isutcnt;
+ uint32_t isstdcnt;
+ uint32_t leapcnt;
+ uint32_t timecnt;
+ uint32_t typecnt;
+ uint32_t charcnt;
+};
+
+#if !HAVE_ATTRIBUTE_PACKED
+#pragma pack(push)
+#endif
+struct
+#if HAVE_ATTRIBUTE_PACKED
+__attribute__((packed))
+#endif
+LocalTimeType {
+ int32_t utoff;
+ uint8_t dst;
+ uint8_t idx;
+};
+
+struct
+#if HAVE_ATTRIBUTE_PACKED
+__attribute__((packed))
+#endif
+LeapSecond {
+ int64_t occur;
+ int32_t corr;
+};
+#if !HAVE_ATTRIBUTE_PACKED
+#pragma pack(pop)
+#endif
+
+struct Data {
+ std::vector<int64_t> transition_times;
+ std::vector<uint8_t> transition_types;
+ std::vector<LocalTimeType> local_time_type_records;
+ std::vector<char> time_zone_designations;
+ std::vector<LeapSecond> leap_second_records;
+ std::vector<uint8_t> standard_or_wall_indicators;
+ std::vector<uint8_t> ut_local_indicators;
+};
+
+inline uint32_t ntoh(uint32_t value) {
+#ifdef WORDS_BIGENDIAN
+ return value;
+#else
+ return bswap_32(value);
+#endif
+}
+
+inline int32_t ntoh(int32_t value) {
+#ifdef WORDS_BIGENDIAN
+ return value;
+#else
+ return bswap_32(value);
+#endif
+}
+
+inline int64_t ntoh(int64_t value) {
+#ifdef WORDS_BIGENDIAN
+ return value;
+#else
+ return bswap_64(value);
+#endif
+}
+
+class TzInfoImpl : public TzInfo {
+public:
+ TzInfoImpl(std::shared_ptr<Logger> logger,
+ std::filesystem::path tzinfo_dir)
+ : logger_(std::move(logger)), base_(std::move(tzinfo_dir)) {
+ static_assert(sizeof(Header) == 44, "Header must be packed");
+ }
+
+ std::optional<time_t> get_local_time(std::string_view tzname,
+ time_t utc_time) const override {
+ if (!base_.empty()) {
+ std::filesystem::path zone = base_ / tzname;
+ auto fd = io::open(zone, io::open_flags::rdonly);
+ if (fd) {
+ return read(zone, fd.get(), utc_time);
+ } else {
+ logger_->warn("Unable to open %s for reading: %s.", zone.c_str(),
+ strerror(errno));
+ }
+ }
+ return std::nullopt;
+ }
+
+private:
+ std::optional<time_t> read(std::filesystem::path const& zone, int fd,
+ time_t utc_time) const {
+ Header header;
+ if (!io::read_all(fd, &header, sizeof(header))) {
+ logger_->warn("%s: Error reading: %s.", zone.c_str(),
+ strerror(errno));
+ return std::nullopt;
+ }
+ if (!check_and_fix_header(header)) {
+ logger_->warn("%s: Not a TZinfo file.", zone.c_str());
+ return std::nullopt;
+ }
+ // Skip V1, 32bit time stamps are just so 90's.
+ auto ret = lseek(fd, data_size(header, '\0'), SEEK_CUR);
+ if (ret == -1) {
+ logger_->warn("%s: Error seeking: %s.", zone.c_str(), strerror(errno));
+ return std::nullopt;
+ }
+ if (!io::read_all(fd, &header, sizeof(header))) {
+ logger_->warn("%s: Error reading: %s.", zone.c_str(),
+ strerror(errno));
+ return std::nullopt;
+ }
+ if (header.ver < '2' || !check_and_fix_header(header)) {
+ logger_->warn("%s: Not a TZinfo V2+ file.", zone.c_str());
+ return std::nullopt;
+ }
+ Data data;
+ if (!read_data(fd, header, data)) {
+ logger_->warn("%s: Error reading: %s.", zone.c_str(),
+ strerror(errno));
+ return std::nullopt;
+ }
+ std::string footer;
+ // Note that read_footer might read past the footer, which is currently
+ // fine (as it is a footer) but good to know for future developers.
+ if (!read_footer(fd, footer)) {
+ logger_->warn("%s: Error reading (4): %s.", zone.c_str(),
+ strerror(errno));
+ return std::nullopt;
+ }
+
+ return utc_to_local(data, footer, utc_time);
+ }
+
+ static std::optional<time_t> utc_to_local(Data const& data,
+ std::string const& footer,
+ time_t utc) {
+ if (data.transition_times.empty())
+ return utc_to_local(footer, utc);
+ if (utc < data.transition_times.front())
+ return std::nullopt;
+ size_t i = 0;
+ while (i < data.transition_times.size() && utc > data.transition_times[i])
+ ++i;
+
+ if (i == 0)
+ return std::nullopt;
+ if (i == data.transition_times.size() && !footer.empty())
+ return utc_to_local(footer, utc);
+ --i;
+
+ return utc_to_local(data.local_time_type_records[
+ data.transition_types[i]], utc);
+ }
+
+ static std::optional<time_t> utc_to_local(LocalTimeType const& local_time,
+ time_t utc) {
+ return utc += local_time.utoff;
+ }
+
+ static std::optional<time_t> utc_to_local(std::string const& tz, time_t utc) {
+ return tz::get_local_time(tz, utc);
+ }
+
+ static size_t data_size(Header const& header, char version) {
+ auto time_size = version >= '2' ? 8 : 4;
+ return header.timecnt * time_size + header.timecnt + header.typecnt * 6 +
+ header.charcnt + header.leapcnt * (time_size + 4) + header.isstdcnt +
+ header.isutcnt;
+ }
+
+ static bool read_data(int fd, Header const& header, Data& data) {
+ assert(header.ver >= '2');
+ data.transition_times.resize(header.timecnt);
+ data.transition_types.resize(header.timecnt);
+ static_assert(sizeof(LocalTimeType) == 6, "LocalTimeType must be packed");
+ data.local_time_type_records.resize(header.typecnt);
+ data.time_zone_designations.resize(header.charcnt);
+ static_assert(sizeof(LeapSecond) == 12, "LeapSecond must be packed");
+ data.leap_second_records.resize(header.leapcnt);
+ data.standard_or_wall_indicators.resize(header.isstdcnt);
+ data.ut_local_indicators.resize(header.isutcnt);
+ if (!io::read_all(fd, data.transition_times.data(),
+ header.timecnt * sizeof(int64_t)) ||
+ !io::read_all(fd, data.transition_types.data(), header.timecnt) ||
+ !io::read_all(fd, data.local_time_type_records.data(),
+ header.typecnt * sizeof(LocalTimeType)) ||
+ !io::read_all(fd, data.time_zone_designations.data(), header.charcnt) ||
+ !io::read_all(fd, data.leap_second_records.data(),
+ header.leapcnt * sizeof(LeapSecond)) ||
+ !io::read_all(fd, data.standard_or_wall_indicators.data(),
+ header.isstdcnt) ||
+ !io::read_all(fd, data.ut_local_indicators.data(), header.isutcnt))
+ return false;
+
+ {
+ int64_t last = std::numeric_limits<int64_t>::min();
+ for (auto& time : data.transition_times) {
+ time = ntoh(time);
+ if (time <= last)
+ return false;
+ last = time;
+ }
+ }
+
+ for (auto const& type : data.transition_types)
+ if (type >= header.typecnt)
+ return false;
+
+ for (auto& local : data.local_time_type_records) {
+ local.utoff = ntoh(local.utoff);
+ if (local.utoff == -2147483648)
+ return false;
+ if (local.dst != 0 && local.dst != 1)
+ return false;
+ if (local.idx >= header.charcnt)
+ return false;
+ }
+
+ if (data.time_zone_designations.empty() ||
+ data.time_zone_designations.back() != '\0')
+ return false;
+
+ {
+ LeapSecond last;
+ last.occur = -1;
+ last.corr = 0;
+ for (auto& leap : data.leap_second_records) {
+ leap.occur = ntoh(leap.occur);
+ leap.corr = ntoh(leap.corr);
+ if (last.occur == -1) {
+ if (leap.occur < 0)
+ return false;
+ } else {
+ if (leap.occur <= last.occur)
+ return false;
+ }
+ auto diff = last.corr - leap.corr;
+ if (diff != 1 && diff != -1)
+ return false;
+ last.occur = leap.occur + 2419199;
+ last.corr = leap.corr;
+ }
+ }
+
+ for (auto const& standard_or_wall : data.standard_or_wall_indicators)
+ if (standard_or_wall != 1 && standard_or_wall != 0)
+ return false;
+
+ for (auto const& ut_or_local : data.ut_local_indicators)
+ if (ut_or_local != 1 && ut_or_local != 0)
+ return false;
+
+ return true;
+ }
+
+ static bool read_footer(int fd, std::string& footer) {
+ size_t offset = 0;
+ footer.resize(128);
+ while (true) {
+ auto got = io::read(fd, footer.data() + offset, footer.size() - offset);
+ if (got <= 0)
+ return false;
+ if (footer.front() != '\n')
+ return false;
+ auto end = footer.substr(0, got).find('\n', std::max(1lu, offset));
+ if (end != std::string::npos) {
+ footer = footer.substr(1, end - 1);
+ return true;
+ }
+ offset += got;
+ footer.resize(offset + 128);
+ }
+ }
+
+ static bool check_and_fix_header(Header& header) {
+ if (memcmp(header.magic, "TZif", 4))
+ return false;
+ header.isutcnt = ntoh(header.isutcnt);
+ header.isstdcnt = ntoh(header.isstdcnt);
+ header.leapcnt = ntoh(header.leapcnt);
+ header.timecnt = ntoh(header.timecnt);
+ header.typecnt = ntoh(header.typecnt);
+ header.charcnt = ntoh(header.charcnt);
+
+ if (header.isutcnt != 0 && header.isutcnt != header.typecnt)
+ return false;
+ if (header.isstdcnt != 0 && header.isstdcnt != header.typecnt)
+ return false;
+ if (header.typecnt == 0)
+ return false;
+ if (header.charcnt == 0)
+ return false;
+
+ return true;
+ }
+
+ std::shared_ptr<Logger> logger_;
+ std::filesystem::path base_;
+};
+
+} // namespace
+
+std::unique_ptr<TzInfo> TzInfo::create(std::shared_ptr<Logger> logger,
+ std::filesystem::path tzinfo_dir) {
+ return std::make_unique<TzInfoImpl>(std::move(logger), std::move(tzinfo_dir));
+}
+
diff --git a/src/tz_info.hh b/src/tz_info.hh
new file mode 100644
index 0000000..eb44e58
--- /dev/null
+++ b/src/tz_info.hh
@@ -0,0 +1,28 @@
+#ifndef TZ_INFO_HH
+#define TZ_INFO_HH
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <string_view>
+
+class Logger;
+
+class TzInfo {
+public:
+ virtual ~TzInfo() = default;
+
+ static std::unique_ptr<TzInfo> create(std::shared_ptr<Logger> logger,
+ std::filesystem::path tzinfo_dir);
+
+ virtual std::optional<time_t> get_local_time(std::string_view tzname,
+ time_t utc_time) const = 0;
+
+
+protected:
+ TzInfo() = default;
+ TzInfo(TzInfo const&) = delete;
+ TzInfo& operator=(TzInfo const&) = delete;
+};
+
+#endif // TZ_INFO_HH
diff --git a/src/tz_str.cc b/src/tz_str.cc
new file mode 100644
index 0000000..207f15d
--- /dev/null
+++ b/src/tz_str.cc
@@ -0,0 +1,265 @@
+#include "common.hh"
+
+#include "tz_str.hh"
+
+#include <math.h>
+
+namespace tz {
+
+namespace {
+
+inline bool is_alpha(char c) {
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
+}
+
+inline bool is_digit(char c) {
+ return c >= '0' && c <= '9';
+}
+
+inline bool is_alphnumeric(char c) {
+ return is_alpha(c) || is_digit(c);
+}
+
+std::optional<std::string_view> read_abbr(std::string_view str,
+ size_t& offset) {
+ if (offset >= str.size())
+ return std::nullopt;
+ auto const start = offset;
+ std::string_view ret;
+ if (str[offset] == '<') {
+ size_t end = str.find('>', offset + 1);
+ if (end == std::string_view::npos)
+ return std::nullopt;
+ offset = end + 1;
+ ret = str.substr(start + 1, end - (start + 1));
+ for (auto const& c : ret) {
+ if (!(is_alphnumeric(c) || c == '+' || c == '-'))
+ return std::nullopt;
+ }
+ } else {
+ while (offset < str.size() && is_alpha(str[offset]))
+ ++offset;
+ ret = str.substr(start, offset - start);
+ }
+ if (ret.size() < 3)
+ return std::nullopt;
+ return ret;
+}
+
+std::optional<uint32_t> read_num(std::string_view str,
+ size_t& offset) {
+ if (offset >= str.size() || !is_digit(str[offset]))
+ return std::nullopt;
+ uint32_t ret = str[offset++] - '0';
+ while (offset < str.size() && is_digit(str[offset])) {
+ auto value = ret * 10 + (str[offset++] - '0');
+ if (value < ret)
+ return std::nullopt;
+ ret = value;
+ }
+ return ret;
+}
+
+std::optional<time_t> read_time(std::string_view str,
+ size_t& offset) {
+ auto hh = read_num(str, offset);
+ if (!hh)
+ return std::nullopt;
+ if (hh.value() > 24)
+ return std::nullopt;
+ time_t ret = hh.value() * 60 * 60;
+ if (offset < str.size() && str[offset] == ':') {
+ ++offset;
+ auto mm = read_num(str, offset);
+ if (!mm)
+ return std::nullopt;
+ if (mm.value() > 59)
+ return std::nullopt;
+ ret += mm.value() * 60;
+ if (offset < str.size() && str[offset] == ':') {
+ ++offset;
+ auto ss = read_num(str, offset);
+ if (!ss)
+ return std::nullopt;
+ if (ss.value() > 59)
+ return std::nullopt;
+ ret += ss.value();
+ }
+ }
+ return ret;
+}
+
+std::optional<time_t> read_offset(std::string_view str,
+ size_t& offset) {
+ bool negative;
+ if (offset < str.size() && (str[offset] == '+' || str[offset] == '-'))
+ negative = str[offset++] == '+'; // Yes, this is correct. ('-' is east)
+ else
+ negative = true; // Yes, this is correct. (default is west)
+ auto ret = read_time(str, offset);
+ if (!ret)
+ return std::nullopt;
+ return negative ? -ret.value() : ret.value();
+}
+
+inline bool leap_year(int32_t local_year) {
+ return (local_year % 4) == 0 &&
+ ((local_year % 100) || ((local_year % 400) == 0));
+}
+
+uint8_t week_day_for_day_of_month(int32_t year, uint8_t month,
+ uint8_t day_of_month) {
+ auto k = static_cast<int>(day_of_month);
+ auto m = month >= 3 ? month - 2 : 10 + month;
+ auto C = year % 100;
+ auto Y = year / 100;
+ if (m > 10)
+ --Y;
+ return abs((k + static_cast<int>((2.6 * m - 0.2))
+ - 2 * C + Y + (Y / 4) + (C / 4))) % 7;
+}
+
+std::optional<time_t> read_date_and_time(std::string_view str,
+ int32_t local_year,
+ size_t& offset) {
+ if (offset >= str.size())
+ return std::nullopt;
+
+ time_t day_of_year;
+ if (str[offset] == 'J') {
+ ++offset;
+ auto julian_day = read_num(str, offset);
+ if (!julian_day || julian_day.value() < 1 || julian_day.value() > 365)
+ return std::nullopt;
+
+ day_of_year = julian_day.value() - 1;
+ if (leap_year(local_year) && julian_day.value() >= 60)
+ ++day_of_year;
+ } else if (str[offset] == 'M') {
+ ++offset;
+ auto month = read_num(str, offset);
+ if (!month || month.value() < 1 || month.value() > 12)
+ return std::nullopt;
+
+ if (offset >= str.size() || str[offset] != '.')
+ return std::nullopt;
+
+ ++offset;
+ auto week_of_month = read_num(str, offset);
+ if (!week_of_month || week_of_month.value() < 1 ||
+ week_of_month.value() > 5)
+ return std::nullopt;
+
+ if (offset >= str.size() || str[offset] != '.')
+ return std::nullopt;
+
+ ++offset;
+ auto day_of_week = read_num(str, offset);
+ if (!day_of_week || day_of_week.value() > 6)
+ return std::nullopt;
+
+ day_of_year = 0;
+ for (size_t i = 1; i < month; ++i) {
+ day_of_year += (i <= 7)
+ ? ((i == 2) ? (leap_year(local_year) ? 29 : 28) : (i % 2 ? 31 : 30))
+ : (i % 2 ? 30 : 31);
+ }
+ auto week_day_for_first_day_of_month = week_day_for_day_of_month(
+ local_year, month.value(), 1);
+ for (size_t i = 1; i < week_of_month; ++i)
+ day_of_year += 7;
+ if (day_of_week.value() < week_day_for_first_day_of_month)
+ day_of_year += 7 - week_day_for_first_day_of_month - day_of_week.value();
+ else
+ day_of_year += day_of_week.value() - week_day_for_first_day_of_month;
+ } else {
+ auto julian_day = read_num(str, offset);
+ if (!julian_day || julian_day.value() > 365)
+ return std::nullopt;
+
+ day_of_year = julian_day.value();
+ }
+
+ time_t ret = day_of_year * 24 * 60 * 60;
+ if (offset < str.size() && str[offset] == '/') {
+ ++offset;
+ auto time = read_time(str, offset);
+ if (!time)
+ return std::nullopt;
+
+ ret += time.value();
+ } else {
+ ret += 2 * 60 * 60;
+ }
+ return ret;
+}
+
+} // namespace
+
+std::optional<time_t> get_local_time(std::string_view tz_str,
+ time_t utc_time) {
+ size_t offset = 0;
+ auto std = read_abbr(tz_str, offset);
+ if (!std)
+ return std::nullopt;
+
+ auto std_offset = read_offset(tz_str, offset);
+ if (!std_offset)
+ return std::nullopt;
+
+ auto local_time = utc_time + std_offset.value();
+ if (offset == tz_str.size())
+ return local_time;
+ auto dst = read_abbr(tz_str, offset);
+ if (!dst)
+ return std::nullopt;
+
+ std::optional<time_t> dst_offset;
+ if (offset == tz_str.size() || tz_str[offset] == ',') {
+ dst_offset = std_offset.value() + 60 * 60;
+ } else {
+ dst_offset = read_offset(tz_str, offset);
+ if (!dst_offset)
+ return std::nullopt;
+ }
+ if (offset == tz_str.size()) {
+ // TODO: Can't figure out what the spec actually says about this.
+ // They are clearly optional but it doesn't specify what the default
+ // are. Assume no DST.
+ return local_time;
+ }
+ if (tz_str[offset] != ',')
+ return std::nullopt;
+ ++offset;
+ // TODO: This can't be 100% correct
+ auto local_year = 1970 + local_time / (365.25 * 24 * 60 * 60);
+ auto start = read_date_and_time(tz_str, local_year, offset);
+ if (!start)
+ return std::nullopt;
+
+ if (tz_str[offset] != ',')
+ return std::nullopt;
+
+ ++offset;
+ auto end = read_date_and_time(tz_str, local_year, offset);
+ if (!end)
+ return std::nullopt;
+
+ // TODO: If local_year isn't correct this is definitly not.
+ auto local_time_in_year =
+ local_time % static_cast<time_t>((365.25 * 24 * 60 * 60));
+ if (start.value() <= end.value()) {
+ if (start.value() < local_time_in_year &&
+ local_time_in_year < end.value()) {
+ return utc_time + dst_offset.value();
+ }
+ } else {
+ if (!(end.value() < local_time_in_year &&
+ local_time_in_year < start.value())) {
+ return utc_time + dst_offset.value();
+ }
+ }
+ return local_time;
+}
+
+} // namespace tz
diff --git a/src/tz_str.hh b/src/tz_str.hh
new file mode 100644
index 0000000..d706197
--- /dev/null
+++ b/src/tz_str.hh
@@ -0,0 +1,16 @@
+#ifndef TZ_STR_HH
+#define TZ_STR_HH
+
+#include <optional>
+#include <string_view>
+
+#include "time.h"
+
+namespace tz {
+
+std::optional<time_t> get_local_time(std::string_view tz_str,
+ time_t utc_time);
+
+} // namespace tz
+
+#endif // TZ_STR_HH
diff --git a/src/unique_fd.cc b/src/unique_fd.cc
new file mode 100644
index 0000000..eb2ed29
--- /dev/null
+++ b/src/unique_fd.cc
@@ -0,0 +1,10 @@
+#include "common.hh"
+
+#include "io.hh"
+#include "unique_fd.hh"
+
+void unique_fd::reset(int fd) {
+ if (fd_ >= 0)
+ io::close(fd_);
+ fd_ = fd;
+}
diff --git a/src/unique_fd.hh b/src/unique_fd.hh
new file mode 100644
index 0000000..dc60b3f
--- /dev/null
+++ b/src/unique_fd.hh
@@ -0,0 +1,48 @@
+#ifndef UNIQUE_FD_HH
+#define UNIQUE_FD_HH
+
+#include <cstddef>
+
+class unique_fd {
+public:
+ constexpr unique_fd() noexcept
+ : fd_(-1) {}
+ constexpr unique_fd(std::nullptr_t) noexcept
+ : fd_(-1) {}
+ explicit unique_fd(int fd) noexcept
+ : fd_(fd) {}
+ unique_fd(unique_fd&& fd) noexcept
+ : fd_(fd.release()) {}
+
+ ~unique_fd() { reset(); }
+
+ unique_fd& operator=(unique_fd&& fd) noexcept {
+ reset(fd.release());
+ return *this;
+ }
+ unique_fd& operator=(std::nullptr_t) noexcept {
+ reset();
+ return *this;
+ }
+
+ int get() const noexcept { return fd_; }
+ int operator*() const { return get(); }
+
+ explicit operator bool() const noexcept { return fd_ >= 0; }
+
+ int release() noexcept {
+ int ret = fd_;
+ fd_ = -1;
+ return ret;
+ }
+
+ void reset(int fd = -1);
+
+private:
+ unique_fd(unique_fd const&) = delete;
+ unique_fd& operator=(unique_fd const&) = delete;
+
+ int fd_;
+};
+
+#endif // UNIQUE_FD_HH
diff --git a/src/unique_pipe.cc b/src/unique_pipe.cc
new file mode 100644
index 0000000..3ab1871
--- /dev/null
+++ b/src/unique_pipe.cc
@@ -0,0 +1,47 @@
+#include "common.hh"
+
+#include "io.hh"
+#include "unique_pipe.hh"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+unique_pipe::unique_pipe(bool non_blocking_reader, bool non_blocking_writer) {
+ int fd[2];
+#if HAVE_PIPE2
+ if (non_blocking_reader == non_blocking_writer) {
+ if (pipe2(fd, non_blocking_reader ? O_NONBLOCK : 0) == 0) {
+ fd_[0] = unique_fd(fd[0]);
+ fd_[1] = unique_fd(fd[1]);
+ }
+ return;
+ }
+#endif
+ if (pipe(fd))
+ return;
+ if (non_blocking_reader)
+ io::make_nonblocking(fd[0]);
+ if (non_blocking_writer)
+ io::make_nonblocking(fd[1]);
+ fd_[0] = unique_fd(fd[0]);
+ fd_[1] = unique_fd(fd[1]);
+}
+
+unique_pipe::unique_pipe(unique_pipe&& fd) noexcept {
+ fd_[0] = unique_fd(fd.fd_[0].release());
+ fd_[1] = unique_fd(fd.fd_[1].release());
+}
+
+void unique_pipe::reset() {
+ fd_[0].reset();
+ fd_[1].reset();
+}
+
+unique_fd unique_pipe::release_reader() {
+ return std::move(fd_[0]);
+}
+
+unique_fd unique_pipe::release_writer() {
+ return std::move(fd_[1]);
+}
+
diff --git a/src/unique_pipe.hh b/src/unique_pipe.hh
new file mode 100644
index 0000000..7020cdf
--- /dev/null
+++ b/src/unique_pipe.hh
@@ -0,0 +1,51 @@
+#ifndef UNIQUE_PIPE_HH
+#define UNIQUE_PIPE_HH
+
+#include <cstddef>
+
+#include "unique_fd.hh"
+
+class unique_pipe {
+public:
+ explicit unique_pipe(bool non_blocking = false)
+ : unique_pipe(non_blocking, non_blocking) {}
+ unique_pipe(bool non_blocking_reader,
+ bool non_blocking_writer);
+ unique_pipe(unique_pipe&& fd) noexcept;
+ unique_pipe(std::nullptr_t) noexcept
+ : fd_{nullptr, nullptr} {}
+
+ ~unique_pipe() = default;
+
+ unique_pipe& operator=(unique_pipe&& fd) noexcept {
+ fd_[0].reset(fd.fd_[0].release());
+ fd_[1].reset(fd.fd_[1].release());
+ return *this;
+ }
+
+ unique_pipe& operator=(std::nullptr_t) noexcept {
+ reset();
+ return *this;
+ }
+
+ int reader() const noexcept { return fd_[0].get(); }
+ int writer() const noexcept { return fd_[1].get(); }
+
+ explicit operator bool() const noexcept {
+ return fd_[0] || fd_[1];
+ }
+
+ void reset();
+
+ unique_fd release_reader();
+ unique_fd release_writer();
+
+private:
+ unique_pipe(unique_pipe const&) = delete;
+ unique_pipe& operator=(unique_pipe const&) = delete;
+
+ unique_fd fd_[2];
+};
+
+
+#endif // UNIQUE_PIPE_HH
diff --git a/src/urlutil.cc b/src/urlutil.cc
new file mode 100644
index 0000000..00ec713
--- /dev/null
+++ b/src/urlutil.cc
@@ -0,0 +1,138 @@
+#include "common.hh"
+
+#include "urlutil.hh"
+
+#include <optional>
+
+namespace url {
+
+namespace {
+
+constexpr char kHex[] = "0123456789ABCDEF";
+
+std::optional<uint8_t> unhex(char c) {
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'A' && c <= 'F')
+ return 10 + (c - 'A');
+ if (c >= 'a' && c <= 'f')
+ return 10 + (c - 'a');
+ return std::nullopt;
+}
+
+bool is_unreserved(char c) {
+ if (c >= 'A' && c <= 'Z')
+ return true;
+ if (c >= 'a' && c <= 'z')
+ return true;
+ if (c >= '0' && c <= '9')
+ return true;
+ return c == '-' || c == '_' || c == '.' || c == '~';
+}
+
+std::string query_unescape(std::string_view str) {
+ std::string ret;
+ size_t start = 0;
+ while (true) {
+ auto next = str.find('+', start);
+ if (next == std::string::npos) {
+ unescape(str.substr(start), ret);
+ break;
+ }
+ unescape(str.substr(start, next - start), ret);
+ ret.push_back(' ');
+ start = next + 1;
+ }
+ return ret;
+}
+
+} // namespace
+
+std::string escape(std::string_view str, EscapeFlags flags) {
+ std::string out;
+ escape(str, out, flags);
+ return out;
+}
+
+void escape(std::string_view str, std::string& out, EscapeFlags flags) {
+ out.reserve(out.size() + str.size());
+ bool const keep_slash =
+ (flags & EscapeFlags::KEEP_SLASH) == EscapeFlags::KEEP_SLASH;
+ for (char c : str) {
+ if (is_unreserved(c) || (c == '/' && keep_slash)) {
+ out.push_back(c);
+ } else {
+ out.push_back('%');
+ out.push_back(kHex[(c & 0xff) >> 4]);
+ out.push_back(kHex[(c & 0xff) & 0xf]);
+ }
+ }
+}
+
+std::string unescape(std::string_view str) {
+ std::string ret;
+ unescape(str, ret);
+ return ret;
+}
+
+void unescape(std::string_view str, std::string& out) {
+ out.reserve(out.size() + str.size());
+
+ size_t last = 0;
+ while (true) {
+ auto next = str.find('%', last);
+ if (next == std::string::npos || next + 3 > str.size())
+ break;
+ auto a = unhex(str[next + 1]);
+ auto b = unhex(str[next + 2]);
+ if (a && b) {
+ out.append(str, last, next - last);
+ out.push_back(a.value() << 4 | b.value());
+ } else {
+ // Keep invalid escape sequences as-is.
+ out.append(str, last, next + 3 - last);
+ }
+ last = next + 3;
+ }
+ out.append(str, last);
+}
+
+void split_and_unescape_path_and_query(
+ std::string_view url,
+ std::string& path,
+ std::unordered_map<std::string, std::string>& query) {
+ auto start = url.find('?');
+ if (start == std::string_view::npos) {
+ path = unescape(url);
+ query.clear();
+ } else {
+ path = unescape(url.substr(0, start));
+ query = expand_and_unescape_query(url.substr(start + 1));
+ }
+}
+
+std::unordered_map<std::string, std::string> expand_and_unescape_query(
+ std::string_view query) {
+ std::unordered_map<std::string, std::string> ret;
+ size_t start = 0;
+ while (true) {
+ auto next = query.find('&', start);
+ auto pair = next == std::string::npos
+ ? query.substr(start)
+ : query.substr(start, next - start);
+ auto eq = pair.find('=');
+ if (eq == std::string::npos) {
+ if (!pair.empty())
+ ret.emplace(query_unescape(pair), std::string());
+ } else {
+ ret.emplace(query_unescape(pair.substr(0, eq)),
+ query_unescape(pair.substr(eq + 1)));
+ }
+ if (next == std::string::npos)
+ break;
+ start = next + 1;
+ }
+ return ret;
+}
+
+} // namespace url
diff --git a/src/urlutil.hh b/src/urlutil.hh
new file mode 100644
index 0000000..5ae170c
--- /dev/null
+++ b/src/urlutil.hh
@@ -0,0 +1,60 @@
+#ifndef URLUTIL_HH
+#define URLUTIL_HH
+
+#include <string>
+#include <string_view>
+#include <unordered_map>
+
+namespace url {
+
+enum class EscapeFlags : unsigned {
+ // Default encodes all non-unreserved characters
+ // (not the same as all reserved) to be safe.
+ DEFAULT = 0,
+ // Same as DEFAULT but doesn't encode SLASH, useful when the in data
+ // is a path.
+ KEEP_SLASH = 1,
+};
+
+std::string escape(std::string_view str,
+ EscapeFlags flags = EscapeFlags::DEFAULT);
+void escape(std::string_view str, std::string& out,
+ EscapeFlags flags = EscapeFlags::DEFAULT);
+
+std::string unescape(std::string_view str);
+void unescape(std::string_view str, std::string& out);
+
+constexpr EscapeFlags operator&(EscapeFlags a, EscapeFlags b) noexcept {
+ using utype = typename std::underlying_type<EscapeFlags>::type;
+ return static_cast<EscapeFlags>(
+ static_cast<utype>(a) & static_cast<utype>(b));
+}
+
+constexpr EscapeFlags operator|(EscapeFlags a, EscapeFlags b) noexcept {
+ using utype = typename std::underlying_type<EscapeFlags>::type;
+ return static_cast<EscapeFlags>(
+ static_cast<utype>(a) | static_cast<utype>(b));
+}
+
+constexpr EscapeFlags operator^(EscapeFlags a, EscapeFlags b) noexcept {
+ using utype = typename std::underlying_type<EscapeFlags>::type;
+ return static_cast<EscapeFlags>(
+ static_cast<utype>(a) ^ static_cast<utype>(b));
+}
+
+constexpr EscapeFlags operator~(EscapeFlags a) noexcept {
+ using utype = typename std::underlying_type<EscapeFlags>::type;
+ return static_cast<EscapeFlags>(~static_cast<utype>(a));
+}
+
+void split_and_unescape_path_and_query(
+ std::string_view url,
+ std::string& path,
+ std::unordered_map<std::string, std::string>& query);
+
+std::unordered_map<std::string, std::string> expand_and_unescape_query(
+ std::string_view query);
+
+} // namespace url
+
+#endif // URLUTIL_HH
diff --git a/src/video.cc b/src/video.cc
new file mode 100644
index 0000000..110bd4a
--- /dev/null
+++ b/src/video.cc
@@ -0,0 +1,171 @@
+#include "common.hh"
+
+#include "strutil.hh"
+#include "timezone.hh"
+#include "video.hh"
+
+#if HAVE_MEDIAINFO
+#include <MediaInfo/MediaInfo.h>
+#endif
+
+namespace {
+
+class VideoImpl : public Video {
+public:
+ VideoImpl(uint64_t width, uint64_t height)
+ : width_(width), height_(height) {}
+
+ uint64_t width() const override {
+ return width_;
+ }
+
+ uint64_t height() const override {
+ return height_;
+ }
+
+ double length() const override {
+ return length_;
+ }
+
+ Location location() const override {
+ return location_;
+ }
+
+ Date date() const override {
+ return date_;
+ }
+
+ void set_length(double length) {
+ length_ = length;
+ }
+
+ void set_location(Location location) {
+ location_ = location;
+ }
+
+ void set_date(Date date) {
+ date_ = date;
+ }
+
+private:
+ uint64_t width_;
+ uint64_t height_;
+ double length_{0.0};
+ Location location_;
+ Date date_;
+};
+
+#if HAVE_MEDIAINFO
+bool parse_location(std::string const& str, Location* location) {
+ auto end = str.find('/');
+ if (end == std::string::npos)
+ return false;
+ char* latend = nullptr;
+ location->lat = strtod(str.c_str(), &latend);
+ if (!latend)
+ return false;
+ char* lngend = nullptr;
+ location->lng = strtod(latend, &lngend);
+ if (!lngend)
+ return false;
+ if (lngend != str.c_str() + end)
+ return false;
+ return true;
+}
+#endif
+
+} // namespace
+
+std::unique_ptr<Video> Video::load(std::filesystem::path const& path,
+ Timezone const* timezone) {
+#if HAVE_MEDIAINFO
+ MediaInfoLib::MediaInfo info;
+#if defined(UNICODE) || defined (_UNICODE)
+ std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> convert;
+ if (info.Open(convert.from_bytes(path))) {
+ auto width = str::parse_uint64(
+ convert.to_bytes(info.Get(MediaInfoLib::Stream_Video, 0, L"Width")));
+ auto height = str::parse_uint64(
+ convert.to_bytes(info.Get(MediaInfoLib::Stream_Video, 0, L"Height")));
+ if (width && height) {
+ std::optional<Location> location;
+ auto video = std::make_unique<VideoImpl>(*width, *height);
+ auto duration = str::parse_uint64(
+ convert.to_bytes(
+ info.Get(MediaInfoLib::Stream_Video, 0, L"Duration")));
+ if (duration)
+ video->set_length(*duration / 1000.0);
+ {
+ Location tmp;
+ if (parse_location(
+ convert.to_bytes(
+ info.Get(MediaInfoLib::Stream_General, 0, L"xyz")),
+ &tmp)) {
+ location = tmp;
+ video->set_location(tmp);
+ }
+ }
+ {
+ auto date = Date::from_format(
+ "UTC %Y-%m-%d %H:%M:%S",
+ convert.to_bytes(
+ info.Get(MediaInfoLib::Stream_General, 0, L"Encoded_Date")),
+ false);
+ if (!date.empty()) {
+ video->set_date(date.value());
+ if (location) {
+ auto local_time = timezone->get_local_time(location->lat,
+ location->lng,
+ date.value());
+ if (local_time)
+ video->set_date(local_time.value());
+ }
+ }
+ }
+ return video;
+ }
+ }
+#else // UNICODE || _UNICODE
+ if (info.Open(path)) {
+ auto width = str::parse_uint64(
+ info.Get(MediaInfoLib::Stream_Video, 0, "Width"));
+ auto height = str::parse_uint64(
+ info.Get(MediaInfoLib::Stream_Video, 0, "Height"));
+ if (width && height) {
+ auto video = std::make_unique<VideoImpl>(*width, *height);
+ auto duration = str::parse_uint64(
+ info.Get(MediaInfoLib::Stream_Video, 0, "Duration"));
+ if (duration)
+ video->set_length(*duration / 1000.0);
+ std::optional<Location> location;
+ {
+ Location tmp;
+ if (parse_location(info.Get(MediaInfoLib::Stream_General, 0, "xyz"),
+ &tmp)) {
+ location = tmp;
+ video->set_location(tmp);
+ }
+ }
+ {
+ auto date = Date::from_format(
+ "UTC %Y-%m-%d %H:%M:%S",
+ info.Get(MediaInfoLib::Stream_General, 0, "Encoded_Date"),
+ false);
+ if (!date.empty()) {
+ video->set_date(date.value());
+ if (location) {
+ auto local_time = timezone->get_local_time(location->lat,
+ location->lng,
+ date.value());
+ if (local_time)
+ video->set_date(local_time.value());
+ }
+ }
+ }
+ return video;
+ }
+ }
+#endif // UNICODE || _UNICODE
+#endif // HAVE_MEDIAINFO
+ return nullptr;
+}
diff --git a/src/video.hh b/src/video.hh
new file mode 100644
index 0000000..26a5c50
--- /dev/null
+++ b/src/video.hh
@@ -0,0 +1,31 @@
+#ifndef VIDEO_HH
+#define VIDEO_HH
+
+#include "date.hh"
+#include "location.hh"
+
+#include <filesystem>
+#include <memory>
+
+class Timezone;
+
+class Video {
+public:
+ virtual ~Video() = default;
+
+ static std::unique_ptr<Video> load(std::filesystem::path const& path,
+ Timezone const* timezone);
+
+ virtual uint64_t width() const = 0;
+ virtual uint64_t height() const = 0;
+ virtual double length() const = 0;
+ virtual Location location() const = 0;
+ virtual Date date() const = 0;
+
+protected:
+ Video() = default;
+ Video(Video const&) = delete;
+ Video& operator=(Video const&) = delete;
+};
+
+#endif // VIDEO_HH
diff --git a/src/weak_ptr.hh b/src/weak_ptr.hh
new file mode 100644
index 0000000..5859d0c
--- /dev/null
+++ b/src/weak_ptr.hh
@@ -0,0 +1,42 @@
+#ifndef WEAK_PTR_HH
+#define WEAK_PTR_HH
+
+#include <memory>
+
+template <typename T>
+class WeakPtr {
+public:
+ explicit WeakPtr(T* ptr)
+ : ptr_(ptr) {}
+
+ void unlink() {
+ ptr_ = nullptr;
+ }
+
+ T* get() {
+ return ptr_;
+ }
+
+private:
+ T* ptr_;
+};
+
+template<typename T>
+class WeakPtrOwner {
+public:
+ explicit WeakPtrOwner(T* ptr)
+ : ptr_(std::make_shared<WeakPtr<T>>(ptr)) {}
+
+ ~WeakPtrOwner() {
+ ptr_->unlink();
+ }
+
+ std::shared_ptr<WeakPtr<T>> get() {
+ return ptr_;
+ }
+
+private:
+ std::shared_ptr<WeakPtr<T>> ptr_;
+};
+
+#endif // WEAK_PTR_HH