LCOV - code coverage report
Current view: top level - http/util - http_date.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 74 74
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 12 12

            Line data    Source code
       1              : #include "http_date.hpp"
       2              : #include <array>
       3              : #include <chrono>
       4              : #include <format>
       5              : 
       6              : namespace thinger::http::util {
       7              : 
       8              : namespace {
       9              : 
      10              :     constexpr std::array<std::string_view, 7> day_names = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
      11              :     constexpr std::array<std::string_view, 7> long_day_names = {"Sunday", "Monday", "Tuesday", "Wednesday",
      12              :                                                                 "Thursday", "Friday", "Saturday"};
      13              :     constexpr std::array<std::string_view, 12> month_names = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
      14              :                                                               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
      15              : 
      16              :     struct date_fields {
      17              :         int year = 0;
      18              :         int month = 0;      // 1-12
      19              :         int day = 0;
      20              :         int hour = 0;
      21              :         int minute = 0;
      22              :         int second = 0;
      23              :     };
      24              : 
      25              :     // Reads the fields of a date from left to right (case-sensitive, as the grammar)
      26              :     class date_reader {
      27              :     public:
      28          228 :         explicit date_reader(std::string_view text) : text_(text) {}
      29              : 
      30         2577 :         bool literal(std::string_view expected) {
      31         2577 :             if (!text_.starts_with(expected)) return false;
      32          879 :             text_.remove_prefix(expected.size());
      33          879 :             return true;
      34              :         }
      35              : 
      36              :         // One of `names`: its index
      37              :         template<size_t N>
      38          327 :         bool name(const std::array<std::string_view, N>& names, int& index) {
      39         1971 :             for (size_t i = 0; i < N; ++i) {
      40         1881 :                 if (literal(names[i])) {
      41          237 :                     index = static_cast<int>(i);
      42          237 :                     return true;
      43              :                 }
      44              :             }
      45           90 :             return false;
      46              :         }
      47              : 
      48              :         // A number of exactly `digits` digits
      49          465 :         bool number(size_t digits, int& value) {
      50          465 :             if (text_.size() < digits) return false;
      51          465 :             value = 0;
      52         1560 :             for (size_t i = 0; i < digits; ++i) {
      53         1104 :                 char c = text_[i];
      54         1104 :                 if (c < '0' || c > '9') return false;
      55         1095 :                 value = value * 10 + (c - '0');
      56              :             }
      57          456 :             text_.remove_prefix(digits);
      58          456 :             return true;
      59              :         }
      60              : 
      61              :         // "HH:MM:SS"
      62           90 :         bool time_of_day(date_fields& fields) {
      63          177 :             return number(2, fields.hour) && literal(":") && number(2, fields.minute) && literal(":") &&
      64          177 :                    number(2, fields.second);
      65              :         }
      66              : 
      67           81 :         bool done() const { return text_.empty(); }
      68              : 
      69              :     private:
      70              :         std::string_view text_;
      71              :     };
      72              : 
      73              :     // IMF-fixdate: "Sun, 06 Nov 1994 08:49:37 GMT"
      74          123 :     bool read_imf_fixdate(std::string_view text, date_fields& fields) {
      75          123 :         date_reader reader(text);
      76          123 :         int weekday = 0;
      77          207 :         if (!(reader.name(day_names, weekday) && reader.literal(", ") && reader.number(2, fields.day) &&
      78           84 :               reader.literal(" ") && reader.name(month_names, fields.month) && reader.literal(" ") &&
      79           84 :               reader.number(4, fields.year) && reader.literal(" ") && reader.time_of_day(fields) &&
      80          201 :               reader.literal(" GMT") && reader.done())) {
      81           54 :             return false;
      82              :         }
      83           69 :         fields.month++;
      84           69 :         return true;
      85              :     }
      86              : 
      87              :     // RFC 850: "Sunday, 06-Nov-94 08:49:37 GMT". The year of two digits is the latest one
      88              :     // ending in them that is not more than 50 years in the future (RFC 9110, 5.6.7).
      89           54 :     bool read_rfc850_date(std::string_view text, date_fields& fields) {
      90           54 :         date_reader reader(text);
      91           54 :         int weekday = 0;
      92           60 :         if (!(reader.name(long_day_names, weekday) && reader.literal(", ") && reader.number(2, fields.day) &&
      93            6 :               reader.literal("-") && reader.name(month_names, fields.month) && reader.literal("-") &&
      94            6 :               reader.number(2, fields.year) && reader.literal(" ") && reader.time_of_day(fields) &&
      95           57 :               reader.literal(" GMT") && reader.done())) {
      96           51 :             return false;
      97              :         }
      98            3 :         fields.month++;
      99              :         using namespace std::chrono;
     100            3 :         int current_year = static_cast<int>(year_month_day{floor<days>(system_clock::now())}.year());
     101            3 :         fields.year += current_year / 100 * 100;
     102            3 :         if (fields.year > current_year + 50) fields.year -= 100;
     103            3 :         return true;
     104              :     }
     105              : 
     106              :     // asctime: "Sun Nov  6 08:49:37 1994" (a day of one digit has a space before)
     107           51 :     bool read_asctime_date(std::string_view text, date_fields& fields) {
     108           51 :         date_reader reader(text);
     109           51 :         int weekday = 0;
     110           69 :         if (!(reader.name(day_names, weekday) && reader.literal(" ") && reader.name(month_names, fields.month) &&
     111           18 :               reader.literal(" ") && (reader.literal(" ") ? reader.number(1, fields.day) : reader.number(2, fields.day)) &&
     112           57 :               reader.literal(" ") && reader.time_of_day(fields) && reader.literal(" ") &&
     113            6 :               reader.number(4, fields.year) && reader.done())) {
     114           45 :             return false;
     115              :         }
     116            6 :         fields.month++;
     117            6 :         return true;
     118              :     }
     119              : 
     120              : }
     121              : 
     122         1084 : std::string format_http_date(std::time_t time) {
     123              :     using namespace std::chrono;
     124         1084 :     sys_seconds point{seconds{time}};
     125         1084 :     auto day_point = floor<days>(point);
     126         1084 :     year_month_day date{day_point};
     127         1084 :     hh_mm_ss clock{point - day_point};
     128              :     return std::format("{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
     129         1084 :                        day_names[weekday{day_point}.c_encoding()], static_cast<unsigned>(date.day()),
     130         1084 :                        month_names[static_cast<unsigned>(date.month()) - 1], static_cast<int>(date.year()),
     131         3252 :                        clock.hours().count(), clock.minutes().count(), clock.seconds().count());
     132              : }
     133              : 
     134          123 : std::optional<std::time_t> parse_http_date(std::string_view date) {
     135          123 :     date_fields fields;
     136          123 :     if (!read_imf_fixdate(date, fields) && !read_rfc850_date(date, fields) && !read_asctime_date(date, fields)) {
     137           45 :         return std::nullopt;
     138              :     }
     139              : 
     140              :     using namespace std::chrono;
     141           78 :     year_month_day day_point{year{fields.year}, month{static_cast<unsigned>(fields.month)},
     142           78 :                              day{static_cast<unsigned>(fields.day)}};
     143              :     // (a second of 60 is a leap second)
     144           78 :     if (!day_point.ok() || fields.hour > 23 || fields.minute > 59 || fields.second > 60) return std::nullopt;
     145           63 :     auto point = sys_days{day_point} + hours{fields.hour} + minutes{fields.minute} + seconds{fields.second};
     146           63 :     return static_cast<std::time_t>(point.time_since_epoch().count());
     147              : }
     148              : 
     149              : }
        

Generated by: LCOV version 2.0-1