You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

css_rule.cxx 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. /*-
  2. * Copyright 2021 Vsevolod Stakhov
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #include "css_rule.hxx"
  17. #include "css.hxx"
  18. #include "libserver/html/html_block.hxx"
  19. #include <limits>
  20. #define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
  21. #include "doctest/doctest.h"
  22. namespace rspamd::css {
  23. /* Class methods */
  24. void css_rule::override_values(const css_rule &other)
  25. {
  26. int bits = 0;
  27. /* Ensure that our bitset is large enough */
  28. static_assert(1 << std::variant_size_v<decltype(css_value::value)> <
  29. std::numeric_limits<int>::max());
  30. for (const auto &v: values) {
  31. bits |= static_cast<int>(1 << v.value.index());
  32. }
  33. for (const auto &ov: other.values) {
  34. if (isset(&bits, static_cast<int>(1 << ov.value.index()))) {
  35. /* We need to override the existing value */
  36. /*
  37. * The algorithm is not very efficient,
  38. * so we need to sort the values first and have a O(N) algorithm
  39. * On the other hand, values vectors are usually limited to the
  40. * number of elements about less then 10, so this O(N^2) algorithm
  41. * is probably ok here
  42. */
  43. for (auto &v: values) {
  44. if (v.value.index() == ov.value.index()) {
  45. v = ov;
  46. }
  47. }
  48. }
  49. }
  50. /* Copy only not set values */
  51. std::copy_if(other.values.begin(), other.values.end(), std::back_inserter(values),
  52. [&bits](const auto &elt) -> bool {
  53. return (bits & (1 << static_cast<int>(elt.value.index()))) == 0;
  54. });
  55. }
  56. void css_rule::merge_values(const css_rule &other)
  57. {
  58. unsigned int bits = 0;
  59. for (const auto &v: values) {
  60. bits |= 1 << v.value.index();
  61. }
  62. /* Copy only not set values */
  63. std::copy_if(other.values.begin(), other.values.end(), std::back_inserter(values),
  64. [&bits](const auto &elt) -> bool {
  65. return (bits & (1 << elt.value.index())) == 0;
  66. });
  67. }
  68. auto css_declarations_block::add_rule(rule_shared_ptr rule) -> bool
  69. {
  70. auto it = rules.find(rule);
  71. auto &&remote_prop = rule->get_prop();
  72. auto ret = true;
  73. if (rule->get_values().size() == 0) {
  74. /* Ignore rules with no values */
  75. return false;
  76. }
  77. if (it != rules.end()) {
  78. auto &&local_rule = *it;
  79. auto &&local_prop = local_rule->get_prop();
  80. if (local_prop.flag == css_property_flag::FLAG_IMPORTANT) {
  81. if (remote_prop.flag == css_property_flag::FLAG_IMPORTANT) {
  82. local_rule->override_values(*rule);
  83. }
  84. else {
  85. /* Override remote not important over local important */
  86. local_rule->merge_values(*rule);
  87. }
  88. }
  89. else if (local_prop.flag == css_property_flag::FLAG_NOT_IMPORTANT) {
  90. if (remote_prop.flag == css_property_flag::FLAG_NOT_IMPORTANT) {
  91. local_rule->override_values(*rule);
  92. }
  93. else {
  94. /* Override local not important over important */
  95. local_rule->merge_values(*rule);
  96. }
  97. }
  98. else {
  99. if (remote_prop.flag == css_property_flag::FLAG_IMPORTANT) {
  100. /* Override with remote */
  101. local_rule->override_values(*rule);
  102. }
  103. else if (remote_prop.flag == css_property_flag::FLAG_NOT_IMPORTANT) {
  104. /* Ignore remote not important over local normal */
  105. ret = false;
  106. }
  107. else {
  108. /* Merge both */
  109. local_rule->merge_values(*rule);
  110. }
  111. }
  112. }
  113. else {
  114. rules.insert(std::move(rule));
  115. }
  116. return ret;
  117. }
  118. }// namespace rspamd::css
  119. namespace rspamd::css {
  120. /* Static functions */
  121. static auto
  122. allowed_property_value(const css_property &prop, const css_consumed_block &parser_block)
  123. -> std::optional<css_value>
  124. {
  125. if (prop.is_color()) {
  126. if (parser_block.is_token()) {
  127. /* A single token */
  128. const auto &tok = parser_block.get_token_or_empty();
  129. if (tok.type == css_parser_token::token_type::hash_token) {
  130. return css_value::maybe_color_from_hex(tok.get_string_or_default(""));
  131. }
  132. else if (tok.type == css_parser_token::token_type::ident_token) {
  133. auto &&ret = css_value::maybe_color_from_string(tok.get_string_or_default(""));
  134. return ret;
  135. }
  136. }
  137. else if (parser_block.is_function()) {
  138. const auto &func = parser_block.get_function_or_invalid();
  139. auto &&ret = css_value::maybe_color_from_function(func);
  140. return ret;
  141. }
  142. }
  143. if (prop.is_dimension()) {
  144. if (parser_block.is_token()) {
  145. /* A single token */
  146. const auto &tok = parser_block.get_token_or_empty();
  147. if (tok.type == css_parser_token::token_type::number_token) {
  148. return css_value::maybe_dimension_from_number(tok);
  149. }
  150. }
  151. }
  152. if (prop.is_display()) {
  153. if (parser_block.is_token()) {
  154. /* A single token */
  155. const auto &tok = parser_block.get_token_or_empty();
  156. if (tok.type == css_parser_token::token_type::ident_token) {
  157. return css_value::maybe_display_from_string(tok.get_string_or_default(""));
  158. }
  159. }
  160. }
  161. if (prop.is_visibility()) {
  162. if (parser_block.is_token()) {
  163. /* A single token */
  164. const auto &tok = parser_block.get_token_or_empty();
  165. if (tok.type == css_parser_token::token_type::ident_token) {
  166. return css_value::maybe_display_from_string(tok.get_string_or_default(""));
  167. }
  168. }
  169. }
  170. if (prop.is_normal_number()) {
  171. if (parser_block.is_token()) {
  172. /* A single token */
  173. const auto &tok = parser_block.get_token_or_empty();
  174. if (tok.type == css_parser_token::token_type::number_token) {
  175. return css_value{tok.get_normal_number_or_default(0)};
  176. }
  177. }
  178. }
  179. return std::nullopt;
  180. }
  181. auto process_declaration_tokens(rspamd_mempool_t *pool,
  182. blocks_gen_functor &&next_block_functor)
  183. -> css_declarations_block_ptr
  184. {
  185. css_declarations_block_ptr ret;
  186. bool can_continue = true;
  187. css_property cur_property{css_property_type::PROPERTY_NYI,
  188. css_property_flag::FLAG_NORMAL};
  189. static const css_property bad_property{css_property_type::PROPERTY_NYI,
  190. css_property_flag::FLAG_NORMAL};
  191. std::shared_ptr<css_rule> cur_rule;
  192. enum {
  193. parse_property,
  194. parse_value,
  195. ignore_value, /* For unknown properties */
  196. } state = parse_property;
  197. auto seen_not = false;
  198. ret = std::make_shared<css_declarations_block>();
  199. while (can_continue) {
  200. const auto &next_tok = next_block_functor();
  201. switch (next_tok.tag) {
  202. case css_consumed_block::parser_tag_type::css_component:
  203. /* Component can be a property or a compound list of values */
  204. if (state == parse_property) {
  205. cur_property = css_property::from_token(next_tok.get_token_or_empty())
  206. .value_or(bad_property);
  207. if (cur_property.type == css_property_type::PROPERTY_NYI) {
  208. state = ignore_value;
  209. /* Ignore everything till ; */
  210. continue;
  211. }
  212. msg_debug_css("got css property: %s", cur_property.to_string());
  213. /* We now expect colon block */
  214. const auto &expect_colon_block = next_block_functor();
  215. if (expect_colon_block.tag != css_consumed_block::parser_tag_type::css_component) {
  216. state = ignore_value; /* Ignore up to the next rule */
  217. }
  218. else {
  219. const auto &expect_colon_tok = expect_colon_block.get_token_or_empty();
  220. if (expect_colon_tok.type != css_parser_token::token_type::colon_token) {
  221. msg_debug_css("invalid rule, no colon after property");
  222. state = ignore_value; /* Ignore up to the next rule */
  223. }
  224. else {
  225. state = parse_value;
  226. cur_rule = std::make_shared<css_rule>(cur_property);
  227. }
  228. }
  229. }
  230. else if (state == parse_value) {
  231. /* Check semicolon */
  232. if (next_tok.is_token()) {
  233. const auto &parser_tok = next_tok.get_token_or_empty();
  234. if (parser_tok.type == css_parser_token::token_type::semicolon_token && cur_rule) {
  235. ret->add_rule(std::move(cur_rule));
  236. state = parse_property;
  237. seen_not = false;
  238. continue;
  239. }
  240. else if (parser_tok.type == css_parser_token::token_type::delim_token) {
  241. if (parser_tok.get_string_or_default("") == "!") {
  242. /* Probably something like !important */
  243. seen_not = true;
  244. }
  245. }
  246. else if (parser_tok.type == css_parser_token::token_type::ident_token) {
  247. if (parser_tok.get_string_or_default("") == "important") {
  248. if (seen_not) {
  249. msg_debug_css("add !important flag to property %s",
  250. cur_property.to_string());
  251. cur_property.flag = css_property_flag::FLAG_NOT_IMPORTANT;
  252. }
  253. else {
  254. msg_debug_css("add important flag to property %s",
  255. cur_property.to_string());
  256. cur_property.flag = css_property_flag::FLAG_IMPORTANT;
  257. }
  258. seen_not = false;
  259. continue;
  260. }
  261. else {
  262. seen_not = false;
  263. }
  264. }
  265. }
  266. auto maybe_value = allowed_property_value(cur_property, next_tok);
  267. if (maybe_value) {
  268. msg_debug_css("added value %s to the property %s",
  269. maybe_value.value().debug_str().c_str(),
  270. cur_property.to_string());
  271. cur_rule->add_value(maybe_value.value());
  272. }
  273. }
  274. else {
  275. /* Ignore all till ; */
  276. if (next_tok.is_token()) {
  277. const auto &parser_tok = next_tok.get_token_or_empty();
  278. if (parser_tok.type == css_parser_token::token_type::semicolon_token) {
  279. state = parse_property;
  280. }
  281. }
  282. }
  283. break;
  284. case css_consumed_block::parser_tag_type::css_function:
  285. if (state == parse_value) {
  286. auto maybe_value = allowed_property_value(cur_property, next_tok);
  287. if (maybe_value && cur_rule) {
  288. msg_debug_css("added value %s to the property %s",
  289. maybe_value.value().debug_str().c_str(),
  290. cur_property.to_string());
  291. cur_rule->add_value(maybe_value.value());
  292. }
  293. }
  294. break;
  295. case css_consumed_block::parser_tag_type::css_eof_block:
  296. if (state == parse_value) {
  297. ret->add_rule(std::move(cur_rule));
  298. }
  299. can_continue = false;
  300. break;
  301. default:
  302. can_continue = false;
  303. break;
  304. }
  305. }
  306. return ret; /* copy elision */
  307. }
  308. auto css_declarations_block::merge_block(const css_declarations_block &other, merge_type how) -> void
  309. {
  310. const auto &other_rules = other.get_rules();
  311. for (auto &rule: other_rules) {
  312. auto &&found_it = rules.find(rule);
  313. if (found_it != rules.end()) {
  314. /* Duplicate, need to merge */
  315. switch (how) {
  316. case merge_type::merge_override:
  317. /* Override */
  318. (*found_it)->override_values(*rule);
  319. break;
  320. case merge_type::merge_duplicate:
  321. /* Merge values */
  322. add_rule(rule);
  323. break;
  324. case merge_type::merge_parent:
  325. /* Do not merge parent rule if more specific local one is presented */
  326. break;
  327. }
  328. }
  329. else {
  330. /* New property, just insert */
  331. rules.insert(rule);
  332. }
  333. }
  334. }
  335. auto css_declarations_block::compile_to_block(rspamd_mempool_t *pool) const -> rspamd::html::html_block *
  336. {
  337. auto *block = rspamd_mempool_alloc0_type(pool, rspamd::html::html_block);
  338. auto opacity = -1;
  339. const css_rule *font_rule = nullptr, *background_rule = nullptr;
  340. for (const auto &rule: rules) {
  341. auto prop = rule->get_prop().type;
  342. const auto &vals = rule->get_values();
  343. if (vals.empty()) {
  344. continue;
  345. }
  346. switch (prop) {
  347. case css_property_type::PROPERTY_VISIBILITY:
  348. case css_property_type::PROPERTY_DISPLAY: {
  349. auto disp = vals.back().to_display().value_or(css_display_value::DISPLAY_INLINE);
  350. block->set_display(disp);
  351. break;
  352. }
  353. case css_property_type::PROPERTY_FONT_SIZE: {
  354. auto fs = vals.back().to_dimension();
  355. if (fs) {
  356. block->set_font_size(fs.value().dim, fs.value().is_percent);
  357. }
  358. }
  359. case css_property_type::PROPERTY_OPACITY: {
  360. opacity = vals.back().to_number().value_or(opacity);
  361. break;
  362. }
  363. case css_property_type::PROPERTY_FONT_COLOR:
  364. case css_property_type::PROPERTY_COLOR: {
  365. auto color = vals.back().to_color();
  366. if (color) {
  367. block->set_fgcolor(color.value());
  368. }
  369. break;
  370. }
  371. case css_property_type::PROPERTY_BGCOLOR: {
  372. auto color = vals.back().to_color();
  373. if (color) {
  374. block->set_bgcolor(color.value());
  375. }
  376. break;
  377. }
  378. case css_property_type::PROPERTY_HEIGHT: {
  379. auto w = vals.back().to_dimension();
  380. if (w) {
  381. block->set_width(w.value().dim, w.value().is_percent);
  382. }
  383. break;
  384. }
  385. case css_property_type::PROPERTY_WIDTH: {
  386. auto h = vals.back().to_dimension();
  387. if (h) {
  388. block->set_width(h.value().dim, h.value().is_percent);
  389. }
  390. break;
  391. }
  392. /* Optional attributes */
  393. case css_property_type::PROPERTY_FONT:
  394. font_rule = rule.get();
  395. break;
  396. case css_property_type::PROPERTY_BACKGROUND:
  397. background_rule = rule.get();
  398. break;
  399. default:
  400. /* Do nothing for now */
  401. break;
  402. }
  403. }
  404. /* Optional properties */
  405. if (!(block->fg_color_mask) && font_rule) {
  406. auto &vals = font_rule->get_values();
  407. for (const auto &val: vals) {
  408. auto maybe_color = val.to_color();
  409. if (maybe_color) {
  410. block->set_fgcolor(maybe_color.value());
  411. }
  412. }
  413. }
  414. if (!(block->font_mask) && font_rule) {
  415. auto &vals = font_rule->get_values();
  416. for (const auto &val: vals) {
  417. auto maybe_dim = val.to_dimension();
  418. if (maybe_dim) {
  419. block->set_font_size(maybe_dim.value().dim, maybe_dim.value().is_percent);
  420. }
  421. }
  422. }
  423. if (!(block->bg_color_mask) && background_rule) {
  424. auto &vals = background_rule->get_values();
  425. for (const auto &val: vals) {
  426. auto maybe_color = val.to_color();
  427. if (maybe_color) {
  428. block->set_bgcolor(maybe_color.value());
  429. }
  430. }
  431. }
  432. return block;
  433. }
  434. void css_rule::add_value(const css_value &value)
  435. {
  436. values.push_back(value);
  437. }
  438. TEST_SUITE("css")
  439. {
  440. TEST_CASE("simple css rules")
  441. {
  442. const std::vector<std::pair<const char *, std::vector<css_property>>> cases{
  443. {"font-size:12.0pt;line-height:115%",
  444. {css_property(css_property_type::PROPERTY_FONT_SIZE)}},
  445. {"font-size:12.0pt;display:none",
  446. {css_property(css_property_type::PROPERTY_FONT_SIZE),
  447. css_property(css_property_type::PROPERTY_DISPLAY)}}};
  448. auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
  449. "css", 0);
  450. for (const auto &c: cases) {
  451. auto res = process_declaration_tokens(pool,
  452. get_rules_parser_functor(pool, c.first));
  453. CHECK(res.get() != nullptr);
  454. for (auto i = 0; i < c.second.size(); i++) {
  455. CHECK(res->has_property(c.second[i]));
  456. }
  457. }
  458. }
  459. }
  460. }// namespace rspamd::css