srctree

Robin Linden parent 6332d745 748eb762
css: Fix invalid CSS barely being handled

Now we'll no longer perform any out-of-bounds reads or hang when fednot-CSS.

inlinesplit
css/parser.cpp added: 92, removed: 22, total 70
@@ -228,11 +228,16 @@ std::optional<std::string_view> try_parse_font_stretch(Tokenizer &tokenizer) {
// Not in header order, but must be defined before Parser::parse_rules() that
// uses this.
template<Predicate T>
constexpr std::string_view Parser::consume_while(T const &pred) {
constexpr std::optional<std::string_view> Parser::consume_while(T const &pred) {
std::size_t start = pos_;
while (pred(input_[pos_])) {
while (!is_eof() && pred(input_[pos_])) {
++pos_;
}
 
if (is_eof()) {
return std::nullopt;
}
 
return input_.substr(start, pos_ - start);
}
 
@@ -247,14 +252,19 @@ std::vector<css::Rule> Parser::parse_rules() {
advance(std::strlen("@media"));
skip_whitespace_and_comments();
 
std::string_view tmp_query = consume_while([](char c) { return c != '{'; });
if (auto last_char = tmp_query.find_last_not_of(' '); last_char != std::string_view::npos) {
tmp_query.remove_suffix(tmp_query.size() - (last_char + 1));
auto tmp_query = consume_while([](char c) { return c != '{'; });
if (!tmp_query) {
spdlog::error("Eof while looking for end of media-query");
return rules;
}
 
if (auto last_char = tmp_query->find_last_not_of(' '); last_char != std::string_view::npos) {
tmp_query->remove_suffix(tmp_query->size() - (last_char + 1));
}
in_media_query = true;
media_query = MediaQuery::parse(tmp_query);
media_query = MediaQuery::parse(*tmp_query);
if (!media_query) {
spdlog::warn("Unable to parse media query: '{}'", tmp_query);
spdlog::warn("Unable to parse media query: '{}'", *tmp_query);
}
consume_char(); // {
skip_whitespace_and_comments();
@@ -264,7 +274,12 @@ std::vector<css::Rule> Parser::parse_rules() {
// @font-face works fine with the normal parsing-logic.
if (starts_with("@") && !starts_with("@font-face")) {
auto kind = consume_while([](char c) { return c != ' ' && c != '{' && c != '('; });
spdlog::warn("Encountered unhandled {} at-rule", kind);
if (!kind) {
spdlog::error("Eof while looking for end of at-rule");
return rules;
}
 
spdlog::warn("Encountered unhandled {} at-rule", *kind);
 
skip_whitespace_and_comments();
std::ignore = consume_while([](char c) { return c != '{'; });
@@ -272,7 +287,11 @@ std::vector<css::Rule> Parser::parse_rules() {
skip_whitespace_and_comments();
 
while (peek() != '}') {
std::ignore = parse_rule();
if (auto rule = parse_rule(); !rule) {
spdlog::error("Eof while looking for end of rule in unknown at-rule");
return rules;
}
 
skip_whitespace_and_comments();
}
 
@@ -281,7 +300,13 @@ std::vector<css::Rule> Parser::parse_rules() {
continue;
}
 
rules.push_back(parse_rule());
auto rule = parse_rule();
if (!rule) {
spdlog::error("Eof while parsing rule");
return rules;
}
 
rules.push_back(*std::move(rule));
rules.back().media_query = media_query;
 
skip_whitespace_and_comments();
@@ -323,6 +348,14 @@ constexpr void Parser::skip_if_neq(char c) {
}
}
 
constexpr std::optional<char> Parser::consume_char() {
if (is_eof()) {
return std::nullopt;
}
 
return input_[pos_++];
}
 
constexpr void Parser::skip_whitespace() {
for (auto c = peek(); c && util::is_whitespace(*c); c = peek()) {
advance(1);
@@ -343,11 +376,15 @@ void Parser::skip_whitespace_and_comments() {
}
}
 
css::Rule Parser::parse_rule() {
std::optional<css::Rule> Parser::parse_rule() {
Rule rule{};
while (peek() != '{') {
auto selector = consume_while([](char c) { return c != ',' && c != '{'; });
rule.selectors.push_back(std::string{util::trim(selector)});
if (!selector) {
return std::nullopt;
}
 
rule.selectors.push_back(std::string{util::trim(*selector)});
skip_if_neq('{'); // ' ' or ','
skip_whitespace_and_comments();
}
@@ -356,7 +393,12 @@ css::Rule Parser::parse_rule() {
skip_whitespace_and_comments();
 
while (peek() != '}') {
auto [name, value] = parse_declaration();
auto decl = parse_declaration();
if (!decl) {
return std::nullopt;
}
 
auto [name, value] = *decl;
add_declaration(rule.declarations, name, value);
skip_whitespace_and_comments();
}
@@ -366,13 +408,21 @@ css::Rule Parser::parse_rule() {
return rule;
}
 
std::pair<std::string_view, std::string_view> Parser::parse_declaration() {
std::optional<std::pair<std::string_view, std::string_view>> Parser::parse_declaration() {
auto name = consume_while([](char c) { return c != ':'; });
if (!name) {
return std::nullopt;
}
 
consume_char(); // :
skip_whitespace_and_comments();
auto value = consume_while([](char c) { return c != ';' && c != '}'; });
if (!value) {
return std::nullopt;
}
 
skip_if_neq('}'); // ;
return {name, value};
return std::pair{*name, *value};
}
 
void Parser::add_declaration(
 
css/parser.h added: 92, removed: 22, total 70
@@ -38,18 +38,18 @@ private:
constexpr bool starts_with(std::string_view) const;
constexpr void advance(std::size_t n) { pos_ += n; }
constexpr void skip_if_neq(char);
constexpr char consume_char() { return input_[pos_++]; }
constexpr std::optional<char> consume_char();
 
template<Predicate T>
constexpr std::string_view consume_while(T const &pred);
constexpr std::optional<std::string_view> consume_while(T const &pred);
 
constexpr void skip_whitespace();
 
// CSS-specific parsing bits.
void skip_whitespace_and_comments();
 
css::Rule parse_rule();
std::pair<std::string_view, std::string_view> parse_declaration();
std::optional<css::Rule> parse_rule();
std::optional<std::pair<std::string_view, std::string_view>> parse_declaration();
 
void add_declaration(
std::map<PropertyId, std::string> &declarations, std::string_view name, std::string_view value) const;
 
css/parser_test.cpp added: 92, removed: 22, total 70
@@ -16,6 +16,7 @@
#include <fmt/format.h>
 
#include <array>
#include <tuple>
 
using namespace std::literals;
using etest::expect;
@@ -996,5 +997,24 @@ int main() {
expect_eq(p.declarations, std::map<css::PropertyId, std::string>{});
});
 
etest::test("parser: incomplete media-query crash", [] {
std::ignore = css::parse("@media("); //
});
 
etest::test("parser: incomplete at-rule crash", [] {
std::ignore = css::parse("@lol"); //
});
 
etest::test("parser: incomplete rule in unknown at-rule crash", [] {
std::ignore = css::parse("@lol ");
std::ignore = css::parse("@lol { p {"); //
});
 
etest::test("parser: incomplete rule crash", [] {
std::ignore = css::parse("p");
std::ignore = css::parse("p {");
std::ignore = css::parse("p { font-size:");
});
 
return etest::run_all_tests();
}