| /** @file | |
| Util functions for combo handler. | |
| @section license License | |
| Licensed to the Apache Software Foundation (ASF) under one | |
| or more contributor license agreements. See the NOTICE file | |
| distributed with this work for additional information | |
| regarding copyright ownership. The ASF licenses this file | |
| to you under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the | |
| License. You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| */ | |
| #include "combo_handler_utils.h" | |
| #include <limits> | |
| namespace | |
| { | |
| constexpr std::string_view MaxAge{"max-age"}; | |
| constexpr std::string_view Private{"private"}; | |
| constexpr std::string_view Immutable{"immutable"}; | |
| // ASCII-only tolower. HTTP tokens are ASCII; std::tolower from <cctype> | |
| // is locale-dependent (notably tr_TR maps 'I' away from 'i') and would | |
| // otherwise let the C locale at plugin init silently change how | |
| // directives like "Immutable" and "PRIVATE" match. | |
| constexpr char | |
| ascii_tolower(char c) | |
| { | |
| return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + ('a' - 'A')) : c; | |
| } | |
| // ASCII-only digit test. std::isdigit from <cctype> is locale-dependent and | |
| // can accept non-ASCII digits when the process locale is not C; HTTP directive | |
| // syntax (e.g. max-age) is ASCII, so match '0'..'9' explicitly. | |
| constexpr bool | |
| ascii_isdigit(char c) | |
| { | |
| return c >= '0' && c <= '9'; | |
| } | |
| bool | |
| starts_with_ignore_case(std::string_view value, std::string_view token) | |
| { | |
| if (value.size() < token.size()) { | |
| return false; | |
| } | |
| for (size_t i = 0; i < token.size(); ++i) { | |
| if (ascii_tolower(value[i]) != ascii_tolower(token[i])) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool | |
| is_lws(char c) | |
| { | |
| return c == ' ' || c == '\t'; | |
| } | |
| // True if `value` begins with `token` (case-insensitive) AND the token is | |
| // terminated by a directive boundary, so a longer word that merely starts | |
| // with the token is not misclassified. The boundary is end-of-string or | |
| // linear whitespace; when `allow_equals` is set (e.g. private="field-name"), | |
| // an '=' introducing a value also terminates the token. This keeps values | |
| // like "privatee" or "immutableX" from matching "private"/"immutable". | |
| bool | |
| matches_directive(std::string_view value, std::string_view token, bool allow_equals) | |
| { | |
| if (!starts_with_ignore_case(value, token)) { | |
| return false; | |
| } | |
| if (value.size() == token.size()) { | |
| return true; | |
| } | |
| char const next = value[token.size()]; | |
| return is_lws(next) || (allow_equals && next == '='); | |
| } | |
| } // namespace | |
| namespace combo_handler | |
| { | |
| CacheControlValue | |
| parse_cache_control_value(std::string_view value) | |
| { | |
| CacheControlValue parsed; | |
| if (starts_with_ignore_case(value, MaxAge)) { | |
| value.remove_prefix(MaxAge.size()); | |
| while (!value.empty() && is_lws(value.front())) { | |
| value.remove_prefix(1); | |
| } | |
| if (!value.empty() && value.front() == '=') { | |
| value.remove_prefix(1); | |
| while (!value.empty() && is_lws(value.front())) { | |
| value.remove_prefix(1); | |
| } | |
| unsigned max_age = 0; | |
| bool overflow = false; | |
| bool any_digit = false; | |
| while (!value.empty() && ascii_isdigit(value.front())) { | |
| unsigned const digit = value.front() - '0'; | |
| if (overflow || max_age > (std::numeric_limits<unsigned>::max() - digit) / 10) { | |
| overflow = true; | |
| } else { | |
| max_age = (max_age * 10) + digit; | |
| } | |
| any_digit = true; | |
| value.remove_prefix(1); | |
| } | |
| // Require at least one digit so that "max-age=" / "max-age=foo" | |
| // don't masquerade as max-age=0. Clamp overflow to UINT_MAX so a | |
| // huge upstream value can't be misread as the smallest possible | |
| // max-age when callers honor zero. | |
| if (any_digit) { | |
| parsed.has_max_age = true; | |
| parsed.max_age = overflow ? std::numeric_limits<unsigned>::max() : max_age; | |
| } | |
| } | |
| } else if (matches_directive(value, Private, /* allow_equals */ true)) { | |
| parsed.is_private = true; | |
| } else if (matches_directive(value, Immutable, /* allow_equals */ false)) { | |
| parsed.is_immutable = true; | |
| } | |
| return parsed; | |
| } | |
| } // namespace combo_handler |