Webthing-CPP: a modern CPP implementation of the WebThings API


LCOV - code coverage report
Current view: top level - webthing - utils.hpp (source / functions) Coverage Total Hit
Test: filtered_coverage.info Lines: 92.3 % 130 120
Test Date: 2025-03-15 12:45:00 Functions: 81.5 % 27 22

            Line data    Source code
       1              : // Webthing-CPP
       2              : // SPDX-FileCopyrightText: 2023-present Benno Waldhauer
       3              : // SPDX-License-Identifier: MIT
       4              : 
       5              : #pragma once
       6              : 
       7              : #include <chrono>
       8              : #include <iomanip>
       9              : #include <iostream>
      10              : #include <mutex>
      11              : #include <optional>
      12              : #include <random>
      13              : #include <regex>
      14              : #include <sstream>
      15              : #include <thread>
      16              : #include <time.h>
      17              : 
      18              : namespace bw::webthing 
      19              : {
      20              : 
      21              : namespace details
      22              : {
      23              :     struct global
      24              :     {
      25              :         static inline std::optional<std::string> fixed_time;
      26              :         static inline std::optional<std::string> fixed_uuid;
      27              :     };
      28              : 
      29              :     // Generate a ISO8601-formatted local time timestamp
      30              :     // and return as std::string
      31      2000630 :     inline std::string current_ISO8601_time_local(const std::optional<std::string>& fixed_time = std::nullopt)
      32              :     {
      33      2000630 :         if(fixed_time)
      34           15 :             return *fixed_time;
      35              : 
      36      2000615 :         auto now = std::chrono::system_clock::now();
      37      2000615 :         std::time_t now_time = std::chrono::system_clock::to_time_t(now);
      38              : 
      39              :         // Get the milliseconds part of the current time
      40      2000615 :         auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() % 1000;
      41              : 
      42              :         // Get the local time
      43              :         std::tm local_time;
      44              : 
      45              :         // Get the timezone offset
      46              :         #ifdef _WIN32
      47              :             localtime_s(&local_time, &now_time);
      48              :             long timezone_offset = 0;
      49              :             int err = _get_timezone(&timezone_offset);
      50              :             if(err != 0)
      51              :                 timezone_offset = 0;
      52              :             timezone_offset = -timezone_offset;
      53              : 
      54              :             if(local_time.tm_isdst)
      55              :                 timezone_offset += 3600;
      56              :         #else
      57      2000615 :             localtime_r(&now_time, &local_time);
      58      2000615 :             long timezone_offset = local_time.tm_gmtoff;
      59              :         #endif
      60              : 
      61              :         // Calculate the timezone offset manually
      62      2000615 :         int offset_hours = timezone_offset / 3600;
      63      2000615 :         int offset_minutes = (std::abs(timezone_offset) % 3600) / 60;
      64              : 
      65              :         // Format the time in ISO 8601 format, including milliseconds
      66      2000615 :         std::ostringstream oss;
      67              :         oss << std::put_time(&local_time, "%Y-%m-%dT%H:%M:%S")
      68      2000615 :             << "." << std::setw(3) << std::setfill('0') << milliseconds;
      69      2000615 :         if (timezone_offset >= 0)
      70      2000615 :             oss << "+";
      71              :         else
      72            0 :             oss << "-";
      73      2000615 :         oss << std::setw(2) << std::setfill('0') << std::abs(offset_hours)
      74      2000615 :             << ":" << std::setw(2) << std::setfill('0') << offset_minutes;
      75              : 
      76      2000615 :         return oss.str();
      77      2000615 :     }
      78              : } // bw::webthing::details
      79              : 
      80      2000068 : inline std::string timestamp()
      81              : {
      82      2000068 :     return details::current_ISO8601_time_local(details::global::fixed_time);
      83              : }
      84              : 
      85              : enum log_level
      86              : {
      87              :     error = 5000,
      88              :     warn  = 4000,
      89              :     info  = 3000,
      90              :     debug = 2000,
      91              :     trace = 1000
      92              : };
      93              : 
      94              : struct logger
      95              : {
      96              :     typedef std::function<void (log_level, const std::string&)> log_impl;
      97              :     typedef std::function<const char* (log_level)> log_color_mapper;
      98              : 
      99            3 :     static void error(const std::string& msg)
     100              :     {
     101            3 :         log(log_level::error, msg);
     102            3 :     }
     103              : 
     104           56 :     static void warn(const std::string& msg)
     105              :     {
     106           56 :         log(log_level::warn, msg);
     107           56 :     }
     108              : 
     109          196 :     static void info(const std::string& msg)
     110              :     {
     111          196 :         log(log_level::info, msg);
     112          196 :     }
     113              : 
     114           80 :     static void debug(const std::string& msg)
     115              :     {
     116           80 :         log(log_level::debug, msg);
     117           80 :     }
     118              : 
     119          238 :     static void trace(const std::string& msg)
     120              :     {
     121          238 :         log(log_level::trace, msg);
     122          238 :     }
     123              : 
     124          573 :     static void log(log_level level, const std::string& msg)
     125              :     {
     126          573 :         if(level < custom_log_level)
     127            4 :             return;
     128              : 
     129          569 :         if(custom_log_impl)
     130            7 :             return custom_log_impl(level, msg);
     131              : 
     132          562 :         default_log_impl(level, msg);
     133              :     }
     134              : 
     135            5 :     static void register_implementation(log_impl log_impl)
     136              :     {
     137            5 :         custom_log_impl = log_impl;
     138            5 :     }
     139              : 
     140           20 :     static void set_level(log_level level)
     141              :     {
     142           20 :         custom_log_level = level;
     143           20 :     }
     144              : 
     145          153 :     static log_level get_level()
     146              :     {
     147          153 :         return custom_log_level;
     148              :     }
     149              : 
     150              :     static void use_color(bool use_color)
     151              :     {
     152              :         log_use_color = use_color;
     153              :     }
     154              : 
     155              : private:
     156          562 :     static void default_log_impl(log_level level, const std::string& msg)
     157              :     {
     158          562 :         auto timestamp = details::current_ISO8601_time_local();
     159          562 :         auto level_str = level == log_level::error ? "ERROR" : 
     160          561 :                          level == log_level::warn  ? "WARN " : 
     161          507 :                          level == log_level::info  ? "INFO " : 
     162          313 :                          level == log_level::debug ? "DEBUG" : 
     163          235 :                          level == log_level::trace ? "TRACE" : 
     164         1686 :                          "L:" + std::to_string(level);
     165              : 
     166         1124 :         std::string color = log_use_color ? log_level_to_color(level) : "";
     167         1124 :         std::string color_clear = log_use_color ? "\033[0m" : "";
     168         1124 :         std::string dim = log_use_color ? "\x1B[2m" : "";
     169         1124 :         std::string dim_off = log_use_color ? "\x1B[22m" : "";
     170          562 :         std::string italic = log_use_color ? "\x1B[1;3m" : "";
     171              : 
     172          562 :         timestamp = std::regex_replace(timestamp, std::regex(R"([T])"), " " + dim_off);
     173          562 :         timestamp = dim + std::regex_replace(timestamp, std::regex{R"([\+])"}, " " + dim + "+");
     174              : 
     175              :         // regex for time, ip 4/6, MAC, urls (with ports)
     176          562 :         auto string_colored =  log_use_color ? (color + std::regex_replace(msg, std::regex{R"("[^"]*"|'[^']*')"}, italic + "$&" + color_clear + color)) : msg;
     177              : 
     178              :         // TODO: make log level configurable for THING (notify e, p, a), WEBSOCKET (in, open, close, broadcast), MDNS, Https (REQ (with/without body), RES)
     179              : 
     180          562 :         std::ostringstream ss;
     181              :         ss << color << timestamp << " [" << "" << std::this_thread::get_id() << "" << "] " <<
     182          562 :         dim_off << level_str << dim  << " -- " << dim_off << string_colored << color_clear;
     183              : 
     184          562 :         std::lock_guard<std::mutex> lg(logger::log_mutex);
     185          562 :         switch (level)
     186              :         {
     187           55 :         case log_level::error:
     188              :         case log_level::warn:
     189           55 :             std::cerr << ss.str() << std::endl;
     190           55 :             break;
     191          507 :         default:
     192          507 :             std::cout << ss.str() << std::endl;
     193              :         }
     194          562 :     }
     195              : 
     196            0 :     static const char* log_level_to_color(log_level level)
     197              :     {
     198            0 :         return  level == log_level::error ? "\x1B[91m" : // light red
     199            0 :                 level == log_level::warn  ? "\x1B[33m" : // yellow
     200            0 :                 level == log_level::info  ? "\x1B[32m" : // green
     201            0 :                 level == log_level::debug ? "\x1B[34m" : // blue
     202            0 :                 level == log_level::trace ? "\x1B[90m" : //light gray
     203            0 :                 "";
     204              :     }
     205              : 
     206              :     static inline std::mutex log_mutex;
     207              :     static inline log_impl custom_log_impl;
     208              :     static inline log_level custom_log_level = log_level::debug;
     209              :     static inline bool log_use_color = false;
     210              : };
     211              : 
     212              : // set a fixed time for timestamp generation
     213              : // this is useful for tests
     214              : // timestamp must follow ISO8601 format
     215              : // e.g. "2023-02-08T01:23:45"
     216            6 : inline void fix_time(std::string timestamp)
     217              : {
     218            6 :   details::global::fixed_time = timestamp;
     219            6 :   logger::warn("time fixed to " + bw::webthing::timestamp()); 
     220            6 : }
     221              : 
     222              : // unfix the time for timestamp generation
     223            6 : inline void unfix_time()
     224              : {
     225            6 :   details::global::fixed_time = std::nullopt;
     226            6 :   logger::warn("time unfixed");
     227            6 : }
     228              : 
     229              : // fix the time for the current scope. Not thread safe!!!
     230              : struct fix_time_scoped
     231              : {
     232            6 :   fix_time_scoped(std::string timestamp)
     233              :   {
     234            6 :     fix_time(timestamp);
     235            6 :   }
     236              : 
     237            6 :   ~fix_time_scoped()
     238              :   {
     239            6 :     unfix_time();
     240            6 :   }
     241              : };
     242              : 
     243              : #define FIXED_TIME_SCOPED(timestamp) auto fixed_time_scope_guard = fix_time_scoped(timestamp);
     244              : 
     245       100041 : inline std::string generate_uuid()
     246              : {
     247       100041 :     if(details::global::fixed_uuid)
     248           13 :         return *details::global::fixed_uuid;
     249              : 
     250              :     // https://stackoverflow.com/a/58467162
     251              :     // TODO: include real uuid generator lib?
     252              : 
     253       100028 :     static std::random_device dev;
     254       100028 :     static std::mt19937 rng(dev());
     255              : 
     256       100028 :     std::uniform_int_distribution<int> dist(0, 15);
     257              : 
     258       100028 :     const char *v = "0123456789abcdef";
     259       100028 :     const bool dash[] = { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0 };
     260              : 
     261       100028 :     std::string uuid;
     262      1700476 :     for (int i = 0; i < 16; i++) {
     263      1600448 :         if (dash[i]) uuid += "-";
     264      1600448 :         uuid += v[dist(rng)];
     265      1600448 :         uuid += v[dist(rng)];
     266              :     }
     267              : 
     268       100028 :     return uuid;
     269       100028 : }
     270              : 
     271              : // set a fixed uuid for uuid generation
     272              : // this is useful for tests
     273            5 : inline void fix_uuid(std::string uuid)
     274              : {
     275            5 :   details::global::fixed_uuid = uuid;
     276            5 :   logger::warn("uuid generation fixed to " + bw::webthing::generate_uuid()); 
     277            5 : }
     278              : 
     279              : // unfix the time for timestamp generation
     280            5 : inline void unfix_uuid()
     281              : {
     282            5 :   details::global::fixed_uuid = std::nullopt;
     283            5 :   logger::warn("uuid generation unfixed");
     284            5 : }
     285              : 
     286              : // fix the uuid generation for the current scope. Not thread safe!!!
     287              : struct fix_uuid_scoped
     288              : {
     289            5 :   fix_uuid_scoped(std::string uuid)
     290              :   {
     291            5 :     fix_uuid(uuid);
     292            5 :   }
     293              : 
     294            5 :   ~fix_uuid_scoped()
     295              :   {
     296            5 :     unfix_uuid();
     297            5 :   }
     298              : };
     299              : 
     300              : #define FIXED_UUID_SCOPED(uuid) auto fixed_uuid_scope_guard = fix_uuid_scoped(uuid);
     301              : 
     302              : // try to static_cast from a type to another, throws std::bad_cast on error
     303              : template<class To, class From>
     304            1 : inline To try_static_cast(const From& value)
     305              : {
     306              :     try
     307              :     {
     308              :         if constexpr (std::is_convertible_v<From, To>)
     309            1 :             return static_cast<To>(value);
     310              :     }
     311            0 :     catch(...)
     312              :     {}
     313              : 
     314            0 :     throw std::bad_cast();
     315              : }
     316              : 
     317              : } // bw::webthing
        

Generated by: LCOV version 2.0-1