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 : }
|