/*- * Copyright 2021 Vsevolod Stakhov * * Licensed 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 "css_value.hxx" #include "css_colors_list.hxx" #include "frozen/unordered_map.h" #include "frozen/string.h" #include "libutil/util.h" #include "contrib/ankerl/unordered_dense.h" #include "fmt/core.h" #define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL #include "doctest/doctest.h" /* Helper for unit test stringification */ namespace doctest { template<> struct StringMaker { static String convert(const rspamd::css::css_color &value) { return fmt::format("r={};g={};b={};alpha={}", value.r, value.g, value.b, value.alpha) .c_str(); } }; }// namespace doctest namespace rspamd::css { auto css_value::maybe_color_from_string(const std::string_view &input) -> std::optional { if (input.size() > 1 && input.front() == '#') { return css_value::maybe_color_from_hex(input.substr(1)); } else { auto found_it = css_colors_map.find(input); if (found_it != css_colors_map.end()) { return css_value{found_it->second}; } } return std::nullopt; } constexpr static inline auto hexpair_decode(char c1, char c2) -> std::uint8_t { std::uint8_t ret = 0; if (c1 >= '0' && c1 <= '9') ret = c1 - '0'; else if (c1 >= 'A' && c1 <= 'F') ret = c1 - 'A' + 10; else if (c1 >= 'a' && c1 <= 'f') ret = c1 - 'a' + 10; ret *= 16; if (c2 >= '0' && c2 <= '9') ret += c2 - '0'; else if (c2 >= 'A' && c2 <= 'F') ret += c2 - 'A' + 10; else if (c2 >= 'a' && c2 <= 'f') ret += c2 - 'a' + 10; return ret; } auto css_value::maybe_color_from_hex(const std::string_view &input) -> std::optional { if (input.length() == 6) { /* Plain RGB */ css_color col(hexpair_decode(input[0], input[1]), hexpair_decode(input[2], input[3]), hexpair_decode(input[4], input[5])); return css_value(col); } else if (input.length() == 3) { /* Rgb as 3 hex digests */ css_color col(hexpair_decode(input[0], input[0]), hexpair_decode(input[1], input[1]), hexpair_decode(input[2], input[2])); return css_value(col); } else if (input.length() == 8) { /* RGBA */ css_color col(hexpair_decode(input[0], input[1]), hexpair_decode(input[2], input[3]), hexpair_decode(input[4], input[5]), hexpair_decode(input[6], input[7])); return css_value(col); } return std::nullopt; } constexpr static inline auto rgb_color_component_convert(const css_parser_token &tok) -> std::uint8_t { std::uint8_t ret = 0; if (tok.type == css_parser_token::token_type::number_token) { auto dbl = std::get(tok.value); if (tok.flags & css_parser_token::number_percent) { if (dbl > 100) { dbl = 100; } else if (dbl < 0) { dbl = 0; } ret = (std::uint8_t)(dbl / 100.0 * 255.0); } else { if (dbl > 255) { dbl = 255; } else if (dbl < 0) { dbl = 0; } ret = (std::uint8_t)(dbl); } } return ret; } constexpr static inline auto alpha_component_convert(const css_parser_token &tok) -> std::uint8_t { double ret = 1.0; if (tok.type == css_parser_token::token_type::number_token) { auto dbl = std::get(tok.value); if (tok.flags & css_parser_token::number_percent) { if (dbl > 100) { dbl = 100; } else if (dbl < 0) { dbl = 0; } ret = (dbl / 100.0); } else { if (dbl > 1.0) { dbl = 1.0; } else if (dbl < 0) { dbl = 0; } ret = dbl; } } return (std::uint8_t)(ret * 255.0); } constexpr static inline auto h_component_convert(const css_parser_token &tok) -> double { double ret = 0.0; if (tok.type == css_parser_token::token_type::number_token) { auto dbl = std::get(tok.value); if (tok.flags & css_parser_token::number_percent) { if (dbl > 100) { dbl = 100; } else if (dbl < 0) { dbl = 0; } ret = (dbl / 100.0); } else { dbl = ((((int) dbl % 360) + 360) % 360); /* Deal with rotations */ ret = dbl / 360.0; /* Normalize to 0..1 */ } } return ret; } constexpr static inline auto sl_component_convert(const css_parser_token &tok) -> double { double ret = 0.0; if (tok.type == css_parser_token::token_type::number_token) { ret = tok.get_normal_number_or_default(ret); } return ret; } static inline auto hsl_to_rgb(double h, double s, double l) -> css_color { css_color ret; constexpr auto hue2rgb = [](auto p, auto q, auto t) -> auto { if (t < 0.0) { t += 1.0; } if (t > 1.0) { t -= 1.0; } if (t * 6. < 1.0) { return p + (q - p) * 6.0 * t; } if (t * 2. < 1) { return q; } if (t * 3. < 2.) { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; } return p; }; if (s == 0) { /* Achromatic */ ret.r = l; ret.g = l; ret.b = l; } else { auto q = l <= 0.5 ? l * (1.0 + s) : l + s - l * s; auto p = 2.0 * l - q; ret.r = (std::uint8_t)(hue2rgb(p, q, h + 1.0 / 3.0) * 255); ret.g = (std::uint8_t)(hue2rgb(p, q, h) * 255); ret.b = (std::uint8_t)(hue2rgb(p, q, h - 1.0 / 3.0) * 255); } ret.alpha = 255; return ret; } auto css_value::maybe_color_from_function(const css_consumed_block::css_function_block &func) -> std::optional { if (func.as_string() == "rgb" && func.args.size() == 3) { css_color col{rgb_color_component_convert(func.args[0]->get_token_or_empty()), rgb_color_component_convert(func.args[1]->get_token_or_empty()), rgb_color_component_convert(func.args[2]->get_token_or_empty())}; return css_value(col); } else if (func.as_string() == "rgba" && func.args.size() == 4) { css_color col{rgb_color_component_convert(func.args[0]->get_token_or_empty()), rgb_color_component_convert(func.args[1]->get_token_or_empty()), rgb_color_component_convert(func.args[2]->get_token_or_empty()), alpha_component_convert(func.args[3]->get_token_or_empty())}; return css_value(col); } else if (func.as_string() == "hsl" && func.args.size() == 3) { auto h = h_component_convert(func.args[0]->get_token_or_empty()); auto s = sl_component_convert(func.args[1]->get_token_or_empty()); auto l = sl_component_convert(func.args[2]->get_token_or_empty()); auto col = hsl_to_rgb(h, s, l); return css_value(col); } else if (func.as_string() == "hsla" && func.args.size() == 4) { auto h = h_component_convert(func.args[0]->get_token_or_empty()); auto s = sl_component_convert(func.args[1]->get_token_or_empty()); auto l = sl_component_convert(func.args[2]->get_token_or_empty()); auto col = hsl_to_rgb(h, s, l); col.alpha = alpha_component_convert(func.args[3]->get_token_or_empty()); return css_value(col); } return std::nullopt; } auto css_value::maybe_dimension_from_number(const css_parser_token &tok) -> std::optional { if (std::holds_alternative(tok.value)) { auto dbl = std::get(tok.value); css_dimension dim; dim.dim = dbl; if (tok.flags & css_parser_token::number_percent) { dim.is_percent = true; } else { dim.is_percent = false; } return css_value{dim}; } return std::nullopt; } constexpr const auto display_names_map = frozen::make_unordered_map({ {"hidden", css_display_value::DISPLAY_HIDDEN}, {"none", css_display_value::DISPLAY_HIDDEN}, {"inline", css_display_value::DISPLAY_INLINE}, {"block", css_display_value::DISPLAY_BLOCK}, {"content", css_display_value::DISPLAY_INLINE}, {"flex", css_display_value::DISPLAY_BLOCK}, {"grid", css_display_value::DISPLAY_BLOCK}, {"inline-block", css_display_value::DISPLAY_INLINE}, {"inline-flex", css_display_value::DISPLAY_INLINE}, {"inline-grid", css_display_value::DISPLAY_INLINE}, {"inline-table", css_display_value::DISPLAY_INLINE}, {"list-item", css_display_value::DISPLAY_BLOCK}, {"run-in", css_display_value::DISPLAY_INLINE}, {"table", css_display_value::DISPLAY_BLOCK}, {"table-caption", css_display_value::DISPLAY_TABLE_ROW}, {"table-column-group", css_display_value::DISPLAY_TABLE_ROW}, {"table-header-group", css_display_value::DISPLAY_TABLE_ROW}, {"table-footer-group", css_display_value::DISPLAY_TABLE_ROW}, {"table-row-group", css_display_value::DISPLAY_TABLE_ROW}, {"table-cell", css_display_value::DISPLAY_TABLE_ROW}, {"table-column", css_display_value::DISPLAY_TABLE_ROW}, {"table-row", css_display_value::DISPLAY_TABLE_ROW}, {"initial", css_display_value::DISPLAY_INLINE}, }); auto css_value::maybe_display_from_string(const std::string_view &input) -> std::optional { auto f = display_names_map.find(input); if (f != display_names_map.end()) { return css_value{f->second}; } return std::nullopt; } auto css_value::debug_str() const -> std::string { std::string ret; std::visit([&](const auto &arg) { using T = std::decay_t; if constexpr (std::is_same_v) { ret += fmt::format("color: r={};g={};b={};alpha={}", arg.r, arg.g, arg.b, arg.alpha); } else if constexpr (std::is_same_v) { ret += "size: " + std::to_string(arg); } else if constexpr (std::is_same_v) { ret += "dimension: " + std::to_string(arg.dim); if (arg.is_percent) { ret += "%"; } } else if constexpr (std::is_same_v) { ret += "display: "; switch (arg) { case css_display_value::DISPLAY_HIDDEN: ret += "hidden"; break; case css_display_value::DISPLAY_BLOCK: ret += "block"; break; case css_display_value::DISPLAY_INLINE: ret += "inline"; break; case css_display_value::DISPLAY_TABLE_ROW: ret += "table_row"; break; } } else if constexpr (std::is_integral_v) { ret += "integral: " + std::to_string(static_cast(arg)); } else { ret += "nyi"; } }, value); return ret; } TEST_SUITE("css"){ TEST_CASE("css hex colors"){ const std::pair hex_tests[] = { {"000", css_color(0, 0, 0)}, {"000000", css_color(0, 0, 0)}, {"f00", css_color(255, 0, 0)}, {"FEDCBA", css_color(254, 220, 186)}, {"234", css_color(34, 51, 68)}, }; for (const auto &p: hex_tests) { SUBCASE((std::string("parse hex color: ") + p.first).c_str()) { auto col_parsed = css_value::maybe_color_from_hex(p.first); //CHECK_UNARY(col_parsed); //CHECK_UNARY(col_parsed.value().to_color()); auto final_col = col_parsed.value().to_color().value(); CHECK(final_col == p.second); } } }// namespace rspamd::css TEST_CASE("css colors strings") { auto passed = 0; for (const auto &p: css_colors_map) { /* Match some of the colors selected randomly */ if (rspamd_random_double_fast() > 0.9) { auto col_parsed = css_value::maybe_color_from_string(p.first); auto final_col = col_parsed.value().to_color().value(); CHECK_MESSAGE(final_col == p.second, p.first.data()); passed++; if (passed > 20) { break; } } } } } ; }