Line data Source code
1 : #include "file_info.hpp"
2 : #include "../../common/http_request.hpp"
3 : #include "../../util/http_date.hpp"
4 : #include "../../../util/ascii.hpp"
5 : #include <algorithm>
6 : #include <format>
7 : #include <sys/stat.h>
8 :
9 : namespace thinger::http {
10 :
11 : namespace {
12 :
13 272 : std::string_view trim_ows(std::string_view value) {
14 272 : auto start = value.find_first_not_of(" \t");
15 272 : if (start == std::string_view::npos) return {};
16 269 : auto end = value.find_last_not_of(" \t");
17 269 : return value.substr(start, end - start + 1);
18 : }
19 :
20 : // A non-negative number of digits only, that fits in 64 bits
21 162 : bool parse_position(std::string_view text, uint64_t& value) {
22 162 : if (text.empty()) return false;
23 159 : value = 0;
24 521 : for (char c : text) {
25 374 : if (c < '0' || c > '9') return false;
26 365 : uint64_t digit = c - '0';
27 365 : if (value > (UINT64_MAX - digit) / 10) return false;
28 362 : value = value * 10 + digit;
29 : }
30 147 : return true;
31 : }
32 :
33 : }
34 :
35 1111 : std::optional<file_info> file_info::of(const std::filesystem::path& path, bool follow_symlinks) {
36 1111 : struct stat st{};
37 1111 : if ((follow_symlinks ? ::stat(path.c_str(), &st) : ::lstat(path.c_str(), &st)) != 0) return std::nullopt;
38 : #ifdef __APPLE__
39 : const auto& modified = st.st_mtimespec;
40 : #else
41 1036 : const auto& modified = st.st_mtim;
42 : #endif
43 1036 : file_info info;
44 1036 : info.regular = S_ISREG(st.st_mode);
45 1036 : info.symlink = S_ISLNK(st.st_mode);
46 1036 : info.size = static_cast<uint64_t>(st.st_size);
47 1036 : info.mtime = modified.tv_sec;
48 1036 : info.mtime_ns = static_cast<uint64_t>(modified.tv_sec) * 1'000'000'000 + static_cast<uint64_t>(modified.tv_nsec);
49 : // nanoseconds: a file rewritten within the same second, with the same size, gets another tag
50 1036 : info.etag = std::format("W/\"{:x}-{:x}\"", info.size, info.mtime_ns);
51 1036 : info.last_modified = util::format_http_date(info.mtime);
52 1036 : return info;
53 1036 : }
54 :
55 674 : bool file_info::not_modified(const http_request& request) const {
56 : // If-None-Match takes precedence: If-Modified-Since is ignored when it is present.
57 : // Repeated, its values make one list.
58 674 : auto if_none_match = request.get_headers_with_key("If-None-Match");
59 674 : if (!if_none_match.empty()) {
60 94 : return std::any_of(if_none_match.begin(), if_none_match.end(),
61 191 : [this](const std::string& value) { return etag_matches(value, etag); });
62 : }
63 :
64 : // Ignored if repeated or not a valid date
65 580 : auto if_modified_since = request.get_headers_with_key("If-Modified-Since");
66 580 : if (if_modified_since.size() != 1) return false;
67 36 : auto since = util::parse_http_date(trim_ows(if_modified_since.front()));
68 36 : return since && mtime <= *since;
69 674 : }
70 :
71 142 : bool etag_matches(std::string_view if_none_match, std::string_view etag) {
72 142 : auto opaque = [](std::string_view tag) {
73 142 : if (tag.starts_with("W/")) tag.remove_prefix(2);
74 142 : return tag;
75 : };
76 142 : auto current = opaque(etag);
77 :
78 : // 1#entity-tag, as W/"opaque" or "opaque" (an opaque tag may hold commas, not quotes)
79 142 : std::string_view rest = if_none_match;
80 : while (true) {
81 193 : auto start = rest.find_first_not_of(" \t,");
82 193 : if (start == std::string_view::npos) return false;
83 154 : rest.remove_prefix(start);
84 154 : if (rest.starts_with('*')) return true;
85 142 : size_t quote = rest.starts_with("W/") ? 2 : 0;
86 142 : if (rest.size() <= quote || rest[quote] != '"') return false;
87 136 : auto close = rest.find('"', quote + 1);
88 136 : if (close == std::string_view::npos) return false;
89 133 : if (rest.substr(quote, close - quote + 1) == current) return true;
90 51 : rest.remove_prefix(close + 1);
91 51 : }
92 : }
93 :
94 121 : std::optional<byte_range> parse_byte_range(std::string_view range, uint64_t size) {
95 121 : range = trim_ows(range);
96 121 : constexpr std::string_view unit = "bytes=";
97 121 : if (!::thinger::util::ascii::istarts_with(range, unit)) return std::nullopt;
98 115 : range = trim_ows(range.substr(unit.size()));
99 :
100 : // Only a single range is served: several of them get the whole resource
101 115 : auto dash = range.find('-');
102 115 : if (dash == std::string_view::npos || range.find(',') != std::string_view::npos) return std::nullopt;
103 105 : auto first_text = range.substr(0, dash);
104 105 : auto last_text = range.substr(dash + 1);
105 :
106 : // "-suffix_length": the last bytes
107 105 : if (first_text.empty()) {
108 19 : uint64_t suffix = 0;
109 19 : if (!parse_position(last_text, suffix)) return std::nullopt;
110 16 : if (suffix == 0 || size == 0) return byte_range{};
111 10 : suffix = std::min(suffix, size);
112 10 : return byte_range{size - suffix, suffix};
113 : }
114 :
115 : // "first-" or "first-last" (a last position past the end is the end)
116 86 : uint64_t first = 0;
117 86 : uint64_t last = UINT64_MAX;
118 86 : if (!parse_position(first_text, first)) return std::nullopt;
119 77 : if (!last_text.empty() && (!parse_position(last_text, last) || last < first)) return std::nullopt;
120 68 : if (first >= size) return byte_range{};
121 52 : last = std::min(last, size - 1);
122 52 : return byte_range{first, last - first + 1};
123 : }
124 :
125 : } // namespace thinger::http
|