LCOV - code coverage report
Current view: top level - http/server/file_server - file_info.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 77 77
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 8 8

            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
        

Generated by: LCOV version 2.0-1