summaryrefslogtreecommitdiff
path: root/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/dirname.cpp16
-rw-r--r--src/utils/dirname.hpp6
-rw-r--r--src/utils/encoding.cpp254
-rw-r--r--src/utils/encoding.hpp43
-rw-r--r--src/utils/get_first_non_empty.cpp11
-rw-r--r--src/utils/get_first_non_empty.hpp20
-rw-r--r--src/utils/optional_bool.hpp35
-rw-r--r--src/utils/reload.cpp2
-rw-r--r--src/utils/revstr.cpp9
-rw-r--r--src/utils/revstr.hpp11
-rw-r--r--src/utils/scopeguard.hpp98
-rw-r--r--src/utils/sha1.cpp40
-rw-r--r--src/utils/sha1.hpp5
-rw-r--r--src/utils/split.cpp19
-rw-r--r--src/utils/split.hpp12
-rw-r--r--src/utils/string.cpp28
-rw-r--r--src/utils/string.hpp8
-rw-r--r--src/utils/system.cpp21
-rw-r--r--src/utils/system.hpp8
-rw-r--r--src/utils/time.cpp80
-rw-r--r--src/utils/time.hpp10
-rw-r--r--src/utils/timed_events.cpp48
-rw-r--r--src/utils/timed_events.hpp137
-rw-r--r--src/utils/timed_events_manager.cpp87
-rw-r--r--src/utils/tolower.cpp13
-rw-r--r--src/utils/tolower.hpp11
-rw-r--r--src/utils/xdg.cpp29
-rw-r--r--src/utils/xdg.hpp12
28 files changed, 1072 insertions, 1 deletions
diff --git a/src/utils/dirname.cpp b/src/utils/dirname.cpp
new file mode 100644
index 0000000..71c9c38
--- /dev/null
+++ b/src/utils/dirname.cpp
@@ -0,0 +1,16 @@
+#include <utils/dirname.hpp>
+
+namespace utils
+{
+ std::string dirname(const std::string filename)
+ {
+ if (filename.empty())
+ return "./";
+ if (filename == ".." || filename == ".")
+ return filename;
+ auto pos = filename.rfind('/');
+ if (pos == std::string::npos)
+ return "./";
+ return filename.substr(0, pos + 1);
+ }
+}
diff --git a/src/utils/dirname.hpp b/src/utils/dirname.hpp
new file mode 100644
index 0000000..c1df81b
--- /dev/null
+++ b/src/utils/dirname.hpp
@@ -0,0 +1,6 @@
+#include <string>
+
+namespace utils
+{
+std::string dirname(const std::string filename);
+}
diff --git a/src/utils/encoding.cpp b/src/utils/encoding.cpp
new file mode 100644
index 0000000..cff0039
--- /dev/null
+++ b/src/utils/encoding.cpp
@@ -0,0 +1,254 @@
+#include <utils/encoding.hpp>
+
+#include <utils/scopeguard.hpp>
+
+#include <stdexcept>
+
+#include <cassert>
+#include <string.h>
+#include <iconv.h>
+#include <cerrno>
+
+#include <map>
+#include <bitset>
+
+/**
+ * The UTF-8-encoded character used as a place holder when a character conversion fails.
+ * This is U+FFFD � "replacement character"
+ */
+static const char* invalid_char = "\xef\xbf\xbd";
+static const size_t invalid_char_len = 3;
+
+namespace utils
+{
+ /**
+ * Based on http://en.wikipedia.org/wiki/UTF-8#Description
+ */
+ std::size_t get_next_codepoint_size(const unsigned char c)
+ {
+ if ((c & 0b11111000) == 0b11110000) // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ return 4;
+ else if ((c & 0b11110000) == 0b11100000) // 3 bytes: 1110xxx 10xxxxxx 10xxxxxx
+ return 3;
+ else if ((c & 0b11100000) == 0b11000000) // 2 bytes: 110xxxxx 10xxxxxx
+ return 2;
+ return 1; // 1 byte: 0xxxxxxx
+ }
+
+ bool is_valid_utf8(const char* s)
+ {
+ if (!s)
+ return false;
+
+ const unsigned char* str = reinterpret_cast<const unsigned char*>(s);
+
+ while (*str)
+ {
+ const auto codepoint_size = get_next_codepoint_size(str[0]);
+ if (codepoint_size == 4)
+ {
+ if (!str[1] || !str[2] || !str[3]
+ || ((str[1] & 0b11000000) != 0b10000000)
+ || ((str[2] & 0b11000000) != 0b10000000)
+ || ((str[3] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if (codepoint_size == 3)
+ {
+ if (!str[1] || !str[2]
+ || ((str[1] & 0b11000000) != 0b10000000)
+ || ((str[2] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if (codepoint_size == 2)
+ {
+ if (!str[1] ||
+ ((str[1] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if ((str[0] & 0b10000000) != 0)
+ return false;
+ str += codepoint_size;
+ }
+ return true;
+ }
+
+ std::string remove_invalid_xml_chars(const std::string& original)
+ {
+ // The given string MUST be a valid utf-8 string
+ std::vector<char> res(original.size(), '\0');
+
+ // pointer where we write valid chars
+ char* r = res.data();
+
+ const char* str = original.c_str();
+ std::bitset<20> codepoint;
+
+ while (*str)
+ {
+ // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ if ((str[0] & 0b11111000) == 0b11110000)
+ {
+ codepoint = ((str[0] & 0b00000111) << 18);
+ codepoint |= ((str[1] & 0b00111111) << 12);
+ codepoint |= ((str[2] & 0b00111111) << 6 );
+ codepoint |= ((str[3] & 0b00111111) << 0 );
+ if (codepoint.to_ulong() <= 0x10FFFF)
+ {
+ ::memcpy(r, str, 4);
+ r += 4;
+ }
+ str += 4;
+ }
+ // 3 bytes: 1110xxx 10xxxxxx 10xxxxxx
+ else if ((str[0] & 0b11110000) == 0b11100000)
+ {
+ codepoint = ((str[0] & 0b00001111) << 12);
+ codepoint |= ((str[1] & 0b00111111) << 6);
+ codepoint |= ((str[2] & 0b00111111) << 0 );
+ if (codepoint.to_ulong() <= 0xD7FF ||
+ (codepoint.to_ulong() >= 0xE000 && codepoint.to_ulong() <= 0xFFFD))
+ {
+ ::memcpy(r, str, 3);
+ r += 3;
+ }
+ str += 3;
+ }
+ // 2 bytes: 110xxxxx 10xxxxxx
+ else if (((str[0]) & 0b11100000) == 0b11000000)
+ {
+ // All 2 bytes char are valid, don't even bother calculating
+ // the codepoint
+ ::memcpy(r, str, 2);
+ r += 2;
+ str += 2;
+ }
+ // 1 byte: 0xxxxxxx
+ else if ((str[0] & 0b10000000) == 0)
+ {
+ codepoint = ((str[0] & 0b01111111));
+ if (codepoint.to_ulong() == 0x09 ||
+ codepoint.to_ulong() == 0x0A ||
+ codepoint.to_ulong() == 0x0D ||
+ codepoint.to_ulong() >= 0x20)
+ {
+ ::memcpy(r, str, 1);
+ r += 1;
+ }
+ str += 1;
+ }
+ else
+ throw std::runtime_error("Invalid UTF-8 passed to remove_invalid_xml_chars");
+ }
+ return {res.data(), static_cast<size_t>(r - res.data())};
+ }
+
+ std::string convert_to_utf8(const std::string& str, const char* charset)
+ {
+ std::string res;
+
+ const iconv_t cd = iconv_open("UTF-8", charset);
+ if (cd == (iconv_t)-1)
+ throw std::runtime_error("Cannot convert into UTF-8");
+
+ // Make sure cd is always closed when we leave this function
+ const auto sg = utils::make_scope_guard([&cd](){ iconv_close(cd); });
+
+ size_t inbytesleft = str.size();
+
+ // iconv will not attempt to modify this buffer, but some plateform
+ // require a char** anyway
+#ifdef ICONV_SECOND_ARGUMENT_IS_CONST
+ const char* inbuf_ptr = str.c_str();
+#else
+ char* inbuf_ptr = const_cast<char*>(str.c_str());
+#endif
+
+ size_t outbytesleft = str.size() * 4;
+ char* outbuf = new char[outbytesleft];
+ char* outbuf_ptr = outbuf;
+
+ // Make sure outbuf is always deleted when we leave this function
+ const auto sg2 = utils::make_scope_guard([outbuf](){ delete[] outbuf; });
+
+ bool done = false;
+ while (done == false)
+ {
+ size_t error = iconv(cd, &inbuf_ptr, &inbytesleft, &outbuf_ptr, &outbytesleft);
+ if ((size_t)-1 == error)
+ {
+ switch (errno)
+ {
+ case EILSEQ:
+ // Invalid byte found. Insert a placeholder instead of the
+ // converted character, jump one byte and continue
+ memcpy(outbuf_ptr, invalid_char, invalid_char_len);
+ outbuf_ptr += invalid_char_len;
+ inbytesleft--;
+ inbuf_ptr++;
+ break;
+ case EINVAL:
+ // A multibyte sequence is not terminated, but we can't
+ // provide any more data, so we just add a placeholder to
+ // indicate that the character is not properly converted,
+ // and we stop the conversion
+ memcpy(outbuf_ptr, invalid_char, invalid_char_len);
+ outbuf_ptr += invalid_char_len;
+ outbuf_ptr++;
+ done = true;
+ break;
+ case E2BIG: // This should never happen
+ default: // This should happen even neverer
+ done = true;
+ break;
+ }
+ }
+ else
+ {
+ // The conversion finished without any error, stop converting
+ done = true;
+ }
+ }
+ // Terminate the converted buffer, and copy that buffer it into the
+ // string we return
+ *outbuf_ptr = '\0';
+ res = outbuf;
+ return res;
+ }
+
+}
+
+namespace xep0106
+{
+ static const std::map<const char, const std::string> encode_map = {
+ {' ', "\\20"},
+ {'"', "\\22"},
+ {'&', "\\26"},
+ {'\'',"\\27"},
+ {'/', "\\2f"},
+ {':', "\\3a"},
+ {'<', "\\3c"},
+ {'>', "\\3e"},
+ {'@', "\\40"},
+ };
+
+ void decode(std::string& s)
+ {
+ std::string::size_type pos;
+ for (const auto& pair: encode_map)
+ while ((pos = s.find(pair.second)) != std::string::npos)
+ s.replace(pos, pair.second.size(),
+ 1, pair.first);
+ }
+
+ void encode(std::string& s)
+ {
+ std::string::size_type pos;
+ while ((pos = s.find_first_of(" \"&'/:<>@")) != std::string::npos)
+ {
+ auto it = encode_map.find(s[pos]);
+ assert(it != encode_map.end());
+ s.replace(pos, 1, it->second);
+ }
+ }
+}
diff --git a/src/utils/encoding.hpp b/src/utils/encoding.hpp
new file mode 100644
index 0000000..b707a0c
--- /dev/null
+++ b/src/utils/encoding.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ /**
+ * Return the size, in bytes, of the next UTF-8 codepoint, based on
+ * the given char.
+ */
+ std::size_t get_next_codepoint_size(const unsigned char c);
+ /**
+ * Returns true if the given null-terminated string is valid utf-8.
+ *
+ * Based on http://en.wikipedia.org/wiki/UTF-8#Description
+ */
+ bool is_valid_utf8(const char* s);
+ /**
+ * Remove all invalid codepoints from the given utf-8-encoded string.
+ * The value returned is a copy of the string, without the removed chars.
+ *
+ * See http://www.w3.org/TR/xml/#charsets for the list of valid characters
+ * in XML.
+ */
+ std::string remove_invalid_xml_chars(const std::string& original);
+ /**
+ * Convert the given string (encoded is "encoding") into valid utf-8.
+ * If some decoding fails, insert an utf-8 placeholder character instead.
+ */
+ std::string convert_to_utf8(const std::string& str, const char* charset);
+}
+
+namespace xep0106
+{
+ /**
+ * Decode and encode inplace.
+ */
+ void decode(std::string&);
+ void encode(std::string&);
+}
+
+
diff --git a/src/utils/get_first_non_empty.cpp b/src/utils/get_first_non_empty.cpp
new file mode 100644
index 0000000..5b3bedb
--- /dev/null
+++ b/src/utils/get_first_non_empty.cpp
@@ -0,0 +1,11 @@
+#include <utils/get_first_non_empty.hpp>
+
+bool is_empty(const std::string& val)
+{
+ return val.empty();
+}
+
+bool is_empty(const int& val)
+{
+ return val == 0;
+}
diff --git a/src/utils/get_first_non_empty.hpp b/src/utils/get_first_non_empty.hpp
new file mode 100644
index 0000000..a38f5fb
--- /dev/null
+++ b/src/utils/get_first_non_empty.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <string>
+
+bool is_empty(const std::string& val);
+bool is_empty(const int& val);
+
+template <typename T>
+T get_first_non_empty(T&& last)
+{
+ return last;
+}
+
+template <typename T, typename... Args>
+T get_first_non_empty(T&& first, Args&&... args)
+{
+ if (!is_empty(first))
+ return first;
+ return get_first_non_empty(std::forward<Args>(args)...);
+}
diff --git a/src/utils/optional_bool.hpp b/src/utils/optional_bool.hpp
new file mode 100644
index 0000000..59bbbab
--- /dev/null
+++ b/src/utils/optional_bool.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <string>
+
+struct OptionalBool
+{
+ OptionalBool() = default;
+
+ OptionalBool(bool value):
+ is_set(true), value(value) {}
+
+ void set_value(bool value)
+ {
+ this->is_set = true;
+ this->value = value;
+ }
+
+ void unset()
+ {
+ this->is_set = false;
+ }
+
+ std::string to_string()
+ {
+ if (this->is_set == false)
+ return "unset";
+ else if (this->value)
+ return "true";
+ else
+ return "false";
+ }
+
+ bool is_set{false};
+ bool value{false};
+};
diff --git a/src/utils/reload.cpp b/src/utils/reload.cpp
index 348c5b5..fdca9bc 100644
--- a/src/utils/reload.cpp
+++ b/src/utils/reload.cpp
@@ -26,7 +26,7 @@ void reload_process()
#ifdef USE_DATABASE
try {
open_database();
- } catch (const litesql::DatabaseError&) {
+ } catch (...) {
log_warning("Re-using the previous database.");
}
#endif
diff --git a/src/utils/revstr.cpp b/src/utils/revstr.cpp
new file mode 100644
index 0000000..87fd801
--- /dev/null
+++ b/src/utils/revstr.cpp
@@ -0,0 +1,9 @@
+#include <utils/revstr.hpp>
+
+namespace utils
+{
+ std::string revstr(const std::string& original)
+ {
+ return {original.rbegin(), original.rend()};
+ }
+}
diff --git a/src/utils/revstr.hpp b/src/utils/revstr.hpp
new file mode 100644
index 0000000..8e521ea
--- /dev/null
+++ b/src/utils/revstr.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ std::string revstr(const std::string& original);
+}
+
+
diff --git a/src/utils/scopeguard.hpp b/src/utils/scopeguard.hpp
new file mode 100644
index 0000000..e697fc3
--- /dev/null
+++ b/src/utils/scopeguard.hpp
@@ -0,0 +1,98 @@
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <vector>
+
+/**
+ * A class to be used to make sure some functions are called when the scope
+ * is left, because they will be called in the ScopeGuard's destructor. It
+ * can for example be used to delete some pointer whenever any exception is
+ * called. Example:
+
+ * {
+ * ScopeGuard scope;
+ * int* number = new int(2);
+ * scope.add_callback([number]() { delete number; });
+ * // Do some other stuff with the number. But these stuff might throw an exception:
+ * throw std::runtime_error("Some error not caught here, but in our caller");
+ * return true;
+ * }
+
+ * In this example, our pointer will always be deleted, even when the
+ * exception is thrown. If we want the functions to be called only when the
+ * scope is left because of an unexpected exception, we can use
+ * ScopeGuard::disable();
+ */
+
+namespace utils
+{
+
+class ScopeGuard
+{
+public:
+ /**
+ * The constructor can take a callback. But additional callbacks can be
+ * added later with add_callback()
+ */
+ explicit ScopeGuard(std::function<void()>&& func):
+ enabled(true)
+ {
+ this->add_callback(std::move(func));
+ }
+
+ ScopeGuard(const ScopeGuard&) = delete;
+ ScopeGuard& operator=(ScopeGuard&&) = delete;
+ ScopeGuard(ScopeGuard&&) = delete;
+ ScopeGuard& operator=(const ScopeGuard&) = delete;
+
+ /**
+ * default constructor, the scope guard is enabled but empty, use
+ * add_callback()
+ */
+ explicit ScopeGuard():
+ enabled(true)
+ {
+ }
+ /**
+ * Call all callbacks in the desctructor, unless it has been disabled.
+ */
+ ~ScopeGuard()
+ {
+ if (this->enabled)
+ for (auto& func: this->callbacks)
+ func();
+ }
+ /**
+ * Add a callback to be called in our destructor, one scope guard can be
+ * used for more than one task, if needed.
+ */
+ void add_callback(std::function<void()>&& func)
+ {
+ this->callbacks.emplace_back(std::move(func));
+ }
+ /**
+ * Disable that scope guard, nothing will be done when the scope is
+ * exited.
+ */
+ void disable()
+ {
+ this->enabled = false;
+ }
+
+private:
+ bool enabled;
+ std::vector<std::function<void()>> callbacks;
+
+};
+
+template<typename F>
+auto make_scope_guard(F f)
+{
+ static struct Empty {} empty;
+ auto deleter = [f = std::move(f)](Empty*) { f(); };
+ return std::unique_ptr<Empty, decltype(deleter)>{&empty, std::move(deleter)};
+}
+
+}
+
diff --git a/src/utils/sha1.cpp b/src/utils/sha1.cpp
new file mode 100644
index 0000000..2e6efc2
--- /dev/null
+++ b/src/utils/sha1.cpp
@@ -0,0 +1,40 @@
+#include <utils/sha1.hpp>
+
+#include <biboumi.h>
+
+#ifdef BOTAN_FOUND
+# include <botan/version.h>
+# include <botan/hash.h>
+# include <botan/hex.h>
+# include <botan/exceptn.h>
+#endif
+#ifdef GCRYPT_FOUND
+# include <gcrypt.h>
+# include <vector>
+# include <iomanip>
+# include <sstream>
+#endif
+
+std::string sha1(const std::string& input)
+{
+#ifdef BOTAN_FOUND
+# if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,11,34)
+ auto sha1 = Botan::HashFunction::create("SHA-1");
+ if (!sha1)
+ throw Botan::Algorithm_Not_Found("SHA-1");
+# else
+ auto sha1 = Botan::HashFunction::create_or_throw("SHA-1");
+# endif
+ sha1->update(input);
+ return Botan::hex_encode(sha1->final(), false);
+#endif
+#ifdef GCRYPT_FOUND
+ const auto hash_length = gcry_md_get_algo_dlen(GCRY_MD_SHA1);
+ std::vector<uint8_t> output(hash_length, {});
+ gcry_md_hash_buffer(GCRY_MD_SHA1, output.data(), input.data(), input.size());
+ std::ostringstream digest;
+ for (std::size_t i = 0; i < hash_length; i++)
+ digest << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(output[i]);
+ return digest.str();
+#endif
+}
diff --git a/src/utils/sha1.hpp b/src/utils/sha1.hpp
new file mode 100644
index 0000000..6c551ac
--- /dev/null
+++ b/src/utils/sha1.hpp
@@ -0,0 +1,5 @@
+#pragma once
+
+#include <string>
+
+std::string sha1(const std::string& input);
diff --git a/src/utils/split.cpp b/src/utils/split.cpp
new file mode 100644
index 0000000..80f8dae
--- /dev/null
+++ b/src/utils/split.cpp
@@ -0,0 +1,19 @@
+#include <utils/split.hpp>
+#include <sstream>
+
+namespace utils
+{
+ std::vector<std::string> split(const std::string& s, const char delim, const bool allow_empty)
+ {
+ std::vector<std::string> ret;
+ std::stringstream ss(s);
+ std::string item;
+ while (std::getline(ss, item, delim))
+ {
+ if (item.empty() && !allow_empty)
+ continue ;
+ ret.emplace_back(std::move(item));
+ }
+ return ret;
+ }
+}
diff --git a/src/utils/split.hpp b/src/utils/split.hpp
new file mode 100644
index 0000000..3755ef8
--- /dev/null
+++ b/src/utils/split.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+
+#include <string>
+#include <vector>
+
+namespace utils
+{
+ std::vector<std::string> split(const std::string &s, const char delim, const bool allow_empty=true);
+}
+
+
diff --git a/src/utils/string.cpp b/src/utils/string.cpp
new file mode 100644
index 0000000..635e71a
--- /dev/null
+++ b/src/utils/string.cpp
@@ -0,0 +1,28 @@
+#include <utils/string.hpp>
+#include <utils/encoding.hpp>
+
+bool to_bool(const std::string& val)
+{
+ return (val == "1" || val == "true");
+}
+
+std::vector<std::string> cut(const std::string& val, const std::size_t size)
+{
+ std::vector<std::string> res;
+ std::string::size_type pos = 0;
+ while (pos < val.size())
+ {
+ // Get the number of chars, <= size, that contain only whole
+ // UTF-8 codepoints.
+ std::size_t s = 0;
+ auto codepoint_size = utils::get_next_codepoint_size(val[pos + s]);
+ while (s + codepoint_size <= size && pos + s < val.size())
+ {
+ s += codepoint_size;
+ codepoint_size = utils::get_next_codepoint_size(val[pos + s]);
+ }
+ res.emplace_back(val.substr(pos, s));
+ pos += s;
+ }
+ return res;
+}
diff --git a/src/utils/string.hpp b/src/utils/string.hpp
new file mode 100644
index 0000000..071ce2c
--- /dev/null
+++ b/src/utils/string.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+
+#include <vector>
+#include <string>
+
+bool to_bool(const std::string& val);
+std::vector<std::string> cut(const std::string& val, const std::size_t size);
diff --git a/src/utils/system.cpp b/src/utils/system.cpp
new file mode 100644
index 0000000..d821dec
--- /dev/null
+++ b/src/utils/system.cpp
@@ -0,0 +1,21 @@
+#include <logger/logger.hpp>
+#include <utils/system.hpp>
+#include <sys/utsname.h>
+#include <cstring>
+
+using namespace std::string_literals;
+
+namespace utils
+{
+std::string get_system_name()
+{
+ struct utsname uts{};
+ const int res = ::uname(&uts);
+ if (res == -1)
+ {
+ log_error("uname failed: ", std::strerror(errno));
+ return "Unknown";
+ }
+ return uts.sysname + " "s + uts.release;
+}
+} \ No newline at end of file
diff --git a/src/utils/system.hpp b/src/utils/system.hpp
new file mode 100644
index 0000000..7ea1677
--- /dev/null
+++ b/src/utils/system.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <string>
+
+namespace utils
+{
+std::string get_system_name();
+} \ No newline at end of file
diff --git a/src/utils/time.cpp b/src/utils/time.cpp
new file mode 100644
index 0000000..bc2c18d
--- /dev/null
+++ b/src/utils/time.cpp
@@ -0,0 +1,80 @@
+#include <utils/time.hpp>
+#include <ctime>
+
+#include <sstream>
+#include <iomanip>
+#include <locale>
+
+#include "biboumi.h"
+
+namespace utils
+{
+std::string to_string(const std::time_t& timestamp)
+{
+ constexpr std::size_t stamp_size = 21;
+ char date_buf[stamp_size];
+ if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(&timestamp)) != stamp_size - 1)
+ return "";
+ return {std::begin(date_buf), std::end(date_buf) - 1};
+}
+
+std::time_t parse_datetime(const std::string& stamp)
+{
+ static const char* format = "%Y-%m-%dT%H:%M:%S";
+ std::tm t = {};
+#ifdef HAS_GET_TIME
+ std::istringstream ss(stamp);
+ ss.imbue(std::locale("C"));
+
+ std::string remainings;
+ ss >> std::get_time(&t, format) >> remainings;
+ if (ss.fail())
+ return -1;
+#else
+ /* Y - m - d T H : M : S */
+ constexpr std::size_t stamp_size_without_tz = 4 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2;
+ if (!strptime(stamp.data(), format, &t)) {
+ return -1;
+ }
+ const std::string remainings(stamp.data() + stamp_size_without_tz);
+#endif
+
+ if (remainings.empty())
+ return -1;
+
+ std::string timezone;
+ // Skip optional fractions of seconds
+ if (remainings[0] == '.')
+ {
+ const auto pos = remainings.find_first_not_of(".0123456789");
+ timezone = remainings.substr(pos);
+ }
+ else
+ timezone = std::move(remainings);
+
+ if (timezone.compare(0, 1, "Z") != 0)
+ {
+ std::stringstream tz_ss;
+ tz_ss << timezone;
+ int multiplier = -1;
+ char prefix;
+ int hours;
+ char sep;
+ int minutes;
+ tz_ss >> prefix >> hours >> sep >> minutes;
+ if (tz_ss.fail())
+ return -1;
+ if (prefix == '-')
+ multiplier = +1;
+ else if (prefix != '+')
+ return -1;
+
+ t.tm_hour += multiplier * hours;
+ t.tm_min += multiplier * minutes;
+ }
+ return ::timegm(&t);
+}
+
+}
+
+
diff --git a/src/utils/time.hpp b/src/utils/time.hpp
new file mode 100644
index 0000000..c71cd9c
--- /dev/null
+++ b/src/utils/time.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <ctime>
+#include <string>
+
+namespace utils
+{
+std::string to_string(const std::time_t& timestamp);
+std::time_t parse_datetime(const std::string& stamp);
+} \ No newline at end of file
diff --git a/src/utils/timed_events.cpp b/src/utils/timed_events.cpp
new file mode 100644
index 0000000..26ded82
--- /dev/null
+++ b/src/utils/timed_events.cpp
@@ -0,0 +1,48 @@
+#include <utility>
+#include <utils/timed_events.hpp>
+
+TimedEvent::TimedEvent(std::chrono::steady_clock::time_point&& time_point,
+ std::function<void()> callback, std::string name):
+ time_point(time_point),
+ callback(std::move(callback)),
+ repeat(false),
+ repeat_delay(0),
+ name(std::move(name))
+{
+}
+
+TimedEvent::TimedEvent(std::chrono::milliseconds&& duration,
+ std::function<void()> callback, std::string name):
+ time_point(std::chrono::steady_clock::now() + duration),
+ callback(std::move(callback)),
+ repeat(true),
+ repeat_delay(duration),
+ name(std::move(name))
+{
+}
+
+bool TimedEvent::is_after(const TimedEvent& other) const
+{
+ return this->is_after(other.time_point);
+}
+
+bool TimedEvent::is_after(const std::chrono::steady_clock::time_point& time_point) const
+{
+ return this->time_point > time_point;
+}
+
+std::chrono::milliseconds TimedEvent::get_timeout() const
+{
+ auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(this->time_point - std::chrono::steady_clock::now());
+ return std::max(diff, 0ms);
+}
+
+void TimedEvent::execute() const
+{
+ this->callback();
+}
+
+const std::string& TimedEvent::get_name() const
+{
+ return this->name;
+}
diff --git a/src/utils/timed_events.hpp b/src/utils/timed_events.hpp
new file mode 100644
index 0000000..fa0fc50
--- /dev/null
+++ b/src/utils/timed_events.hpp
@@ -0,0 +1,137 @@
+#pragma once
+
+#include <functional>
+#include <string>
+#include <chrono>
+#include <vector>
+
+using namespace std::literals::chrono_literals;
+
+namespace utils {
+static constexpr std::chrono::milliseconds no_timeout = std::chrono::milliseconds(-1);
+}
+
+class TimedEventsManager;
+
+/**
+ * A callback with an associated date.
+ */
+
+class TimedEvent
+{
+ friend class TimedEventsManager;
+public:
+ /**
+ * An event the occurs only once, at the given time_point
+ */
+ explicit TimedEvent(std::chrono::steady_clock::time_point&& time_point,
+ std::function<void()> callback, std::string name="");
+ explicit TimedEvent(std::chrono::milliseconds&& duration,
+ std::function<void()> callback, std::string name="");
+
+ explicit TimedEvent(TimedEvent&&) = default;
+ TimedEvent& operator=(TimedEvent&&) = default;
+ ~TimedEvent() = default;
+
+ TimedEvent(const TimedEvent&) = delete;
+ TimedEvent& operator=(const TimedEvent&) = delete;
+
+ /**
+ * Whether or not this event happens after the other one.
+ */
+ bool is_after(const TimedEvent& other) const;
+ bool is_after(const std::chrono::steady_clock::time_point& time_point) const;
+ /**
+ * Return the duration difference between now and the event time point.
+ * If the difference would be negative (i.e. the event is expired), the
+ * returned value is 0 instead. The value cannot then be negative.
+ */
+ std::chrono::milliseconds get_timeout() const;
+ void execute() const;
+ const std::string& get_name() const;
+
+private:
+ /**
+ * The next time point at which the event is executed.
+ */
+ std::chrono::steady_clock::time_point time_point;
+ /**
+ * The function to execute.
+ */
+ std::function<void()> callback;
+ /**
+ * Whether or not this events repeats itself until it is destroyed.
+ */
+ bool repeat;
+ /**
+ * This value is added to the time_point each time the event is executed,
+ * if repeat is true. Otherwise it is ignored.
+ */
+ std::chrono::milliseconds repeat_delay;
+ /**
+ * A name that is used to identify that event. If you want to find your
+ * event (for example if you want to cancel it), the name should be
+ * unique.
+ */
+ std::string name;
+};
+
+/**
+ * A class managing a list of TimedEvents.
+ * They are sorted, new events can be added, removed, fetch, etc.
+ */
+
+class TimedEventsManager
+{
+public:
+ ~TimedEventsManager() = default;
+
+ TimedEventsManager(const TimedEventsManager&) = delete;
+ TimedEventsManager(TimedEventsManager&&) = delete;
+ TimedEventsManager& operator=(const TimedEventsManager&) = delete;
+ TimedEventsManager& operator=(TimedEventsManager&&) = delete;
+
+ /**
+ * Return the unique instance of this class
+ */
+ static TimedEventsManager& instance();
+ /**
+ * Add an event to the list of managed events. The list is sorted after
+ * this call.
+ */
+ void add_event(TimedEvent&& event);
+ /**
+ * Returns the duration, in milliseconds, between now and the next
+ * available event. If the event is already expired (the duration is
+ * negative), 0 is returned instead (as in “it's not too late, execute it
+ * now”)
+ * Returns a negative value if no event is available.
+ */
+ std::chrono::milliseconds get_timeout() const;
+ /**
+ * Execute all the expired events (if their expiration time is exactly
+ * now, or before now). The event is then removed from the list. If the
+ * event does repeat, its expiration time is updated and it is reinserted
+ * in the list at the correct position.
+ * Returns the number of executed events.
+ */
+ std::size_t execute_expired_events();
+ /**
+ * Remove (and thus cancel) all the timed events with the given name.
+ * Returns the number of canceled events.
+ */
+ std::size_t cancel(const std::string& name);
+ /**
+ * Return the number of managed events.
+ */
+ std::size_t size() const;
+ /**
+ * Return a pointer to the first event with the given name. If none
+ * is found, returns nullptr.
+ */
+ const TimedEvent* find_event(const std::string& name) const;
+
+private:
+ std::vector<TimedEvent> events;
+ explicit TimedEventsManager() = default;
+};
diff --git a/src/utils/timed_events_manager.cpp b/src/utils/timed_events_manager.cpp
new file mode 100644
index 0000000..75e6338
--- /dev/null
+++ b/src/utils/timed_events_manager.cpp
@@ -0,0 +1,87 @@
+#include <utils/timed_events.hpp>
+
+#include <algorithm>
+
+TimedEventsManager& TimedEventsManager::instance()
+{
+ static TimedEventsManager inst;
+ return inst;
+}
+
+void TimedEventsManager::add_event(TimedEvent&& event)
+{
+ for (auto it = this->events.begin(); it != this->events.end(); ++it)
+ {
+ if (it->is_after(event))
+ {
+ this->events.emplace(it, std::move(event));
+ return;
+ }
+ }
+ this->events.emplace_back(std::move(event));
+}
+
+std::chrono::milliseconds TimedEventsManager::get_timeout() const
+{
+ if (this->events.empty())
+ return utils::no_timeout;
+ return this->events.front().get_timeout();
+}
+
+std::size_t TimedEventsManager::execute_expired_events()
+{
+ std::size_t count = 0;
+ const auto now = std::chrono::steady_clock::now();
+ for (auto it = this->events.begin(); it != this->events.end();)
+ {
+ if (!it->is_after(now))
+ {
+ TimedEvent copy(std::move(*it));
+ it = this->events.erase(it);
+ ++count;
+ copy.execute();
+ if (copy.repeat)
+ {
+ copy.time_point += copy.repeat_delay;
+ this->add_event(std::move(copy));
+ }
+ continue;
+ }
+ else
+ break;
+ }
+ return count;
+}
+
+std::size_t TimedEventsManager::cancel(const std::string& name)
+{
+ std::size_t res = 0;
+ for (auto it = this->events.begin(); it != this->events.end();)
+ {
+ if (it->get_name() == name)
+ {
+ it = this->events.erase(it);
+ res++;
+ }
+ else
+ ++it;
+ }
+ return res;
+}
+
+
+
+std::size_t TimedEventsManager::size() const
+{
+ return this->events.size();
+}
+
+const TimedEvent* TimedEventsManager::find_event(const std::string& name) const
+{
+ const auto it = std::find_if(this->events.begin(), this->events.end(), [&name](const TimedEvent& o) {
+ return o.get_name() == name;
+ });
+ if (it == this->events.end())
+ return nullptr;
+ return &*it;
+}
diff --git a/src/utils/tolower.cpp b/src/utils/tolower.cpp
new file mode 100644
index 0000000..3e518bd
--- /dev/null
+++ b/src/utils/tolower.cpp
@@ -0,0 +1,13 @@
+#include <utils/tolower.hpp>
+
+namespace utils
+{
+ std::string tolower(const std::string& original)
+ {
+ std::string res;
+ res.reserve(original.size());
+ for (const char c: original)
+ res += static_cast<char>(std::tolower(c));
+ return res;
+ }
+}
diff --git a/src/utils/tolower.hpp b/src/utils/tolower.hpp
new file mode 100644
index 0000000..650e05d
--- /dev/null
+++ b/src/utils/tolower.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ std::string tolower(const std::string& original);
+}
+
+
diff --git a/src/utils/xdg.cpp b/src/utils/xdg.cpp
new file mode 100644
index 0000000..b0fa7be
--- /dev/null
+++ b/src/utils/xdg.cpp
@@ -0,0 +1,29 @@
+#include <utils/xdg.hpp>
+#include <cstdlib>
+
+#include "biboumi.h"
+
+std::string xdg_path(const std::string& filename, const char* env_var)
+{
+ const char* xdg_home = ::getenv(env_var);
+ if (xdg_home && xdg_home[0] == '/')
+ return std::string{xdg_home} + "/" PROJECT_NAME "/" + filename;
+ else
+ {
+ const char* home = ::getenv("HOME");
+ if (home)
+ return std::string{home} + "/" ".config" "/" PROJECT_NAME "/" + filename;
+ else
+ return filename;
+ }
+}
+
+std::string xdg_config_path(const std::string& filename)
+{
+ return xdg_path(filename, "XDG_CONFIG_HOME");
+}
+
+std::string xdg_data_path(const std::string& filename)
+{
+ return xdg_path(filename, "XDG_DATA_HOME");
+}
diff --git a/src/utils/xdg.hpp b/src/utils/xdg.hpp
new file mode 100644
index 0000000..7be6922
--- /dev/null
+++ b/src/utils/xdg.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+
+#include <string>
+
+/**
+ * Returns a path for the given filename, according to the XDG base
+ * directory specification, see
+ * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+ */
+std::string xdg_config_path(const std::string& filename);
+std::string xdg_data_path(const std::string& filename);