#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>
#include "log.hpp"
#include "re2.h"
#include "tokenizer/pgsql.hpp"
#include "tokenizer/sql_base.hpp"
#include "utils.hpp"
using namespace std::literals;
namespace ddwaf {
namespace {
constexpr std::string_view identifier_regex_initialiser =
R"((?i)^(?:(?P<keyword>SELECT|FROM|WHERE|GROUP|OFFSET|LIMIT|HAVING|ORDER|PARTITION|BY|ASC|DESC|NULL)|^(?P<binary_operator>OR|XOR|AND|IN|BETWEEN|LIKE|REGEXP|SOUNDS|LIKE|NOT|IS|MOD|DIV)|^(?P<identifier>[\x{0080}-\x{FFFF}a-zA-Z_][\x{0080}-\x{FFFF}a-zA-Z_0-9$]*))(?:\b|\s|$))";
constexpr std::string_view parameter_regex_initialiser(R"(^(?P<parameter>\$[0-9]+)(?:\b|\s|$))");
std::unique_ptr<re2::RE2> identifier_regex;
std::unique_ptr<re2::RE2> parameter_regex;
}
bool pgsql_tokenizer::initialise_regexes()
{
static const bool ret = []() {
try {
const bool parent_init = sql_tokenizer<pgsql_tokenizer>::initialise_regexes();
identifier_regex = std::make_unique<re2::RE2>(identifier_regex_initialiser);
parameter_regex = std::make_unique<re2::RE2>(parameter_regex_initialiser);
return parent_init && identifier_regex->ok() && parameter_regex->ok();
} catch (...) {
return false;
}
}();
return ret;
}
pgsql_tokenizer::pgsql_tokenizer(
std::string_view str, std::unordered_set<sql_token_type> skip_tokens)
: sql_tokenizer(str, std::move(skip_tokens))
{
static const bool ret = []() {
identifier_regex = std::make_unique<re2::RE2>(identifier_regex_initialiser);
parameter_regex = std::make_unique<re2::RE2>(parameter_regex_initialiser);
return identifier_regex->ok() && parameter_regex->ok();
}();
if (!ret) {
if (!identifier_regex->ok()) {
throw std::runtime_error(
"pgsql identifier regex not valid: " + identifier_regex->error_arg());
}
if (!parameter_regex->ok()) {
throw std::runtime_error(
"pgsql parameter regex not valid: " + parameter_regex->error_arg());
}
throw std::runtime_error("unknown failure on pgsql regex initialisation");
}
}
void pgsql_tokenizer::tokenize_string_keyword_operator_or_identifier()
{
sql_token token;
token.index = index();
auto c = ddwaf::tolower(peek());
auto n = next();
auto n2 = next(2);
if (c == 'u' && n == '&' && (n2 == '\'' || n2 == '"')) {
advance(2);
token.str = extract_conforming_string(n2);
token.type = n2 == '\'' ? sql_token_type::single_quoted_string : sql_token_type::identifier;
emplace_token(token);
return;
}
if (c == 'e' && n == '\'') {
advance();
token.str = extract_escaped_string('\'');
token.type = sql_token_type::single_quoted_string;
emplace_token(token);
return;
}
if ((c == 'b' || c == 'x') && n == '\'') {
advance();
token.str = extract_unescaped_string('\'');
token.type = sql_token_type::number;
emplace_token(token);
return;
}
auto remaining_str = substr();
std::string_view binary_op;
std::string_view keyword;
std::string_view ident;
const std::string_view ref(remaining_str.data(), remaining_str.size());
if (re2::RE2::PartialMatch(ref, *identifier_regex, &keyword, &binary_op, &ident)) {
if (!binary_op.empty()) {
token.type = sql_token_type::binary_operator;
token.str = substr(token.index, binary_op.size());
advance(token.str.size() - 1);
} else if (!keyword.empty()) {
token.type = sql_token_type::keyword;
token.str = substr(token.index, keyword.size());
advance(token.str.size() - 1);
} else if (!ident.empty()) {
token.type = sql_token_type::identifier;
token.str = substr(token.index, ident.size());
advance(token.str.size() - 1);
}
emplace_token(token);
}
}
void pgsql_tokenizer::tokenize_inline_comment_or_operator()
{
sql_token token;
token.index = index();
if (advance() && peek() == '*') {
advance(); while (advance()) {
if (prev() == '*' && peek() == '/') {
break;
}
}
token.str = substr(token.index, index() - token.index + 1);
token.type = sql_token_type::inline_comment;
} else {
token.str = substr(token.index, 1);
token.type = sql_token_type::binary_operator;
}
emplace_token(token);
}
void pgsql_tokenizer::tokenize_eol_comment()
{
sql_token token;
token.index = index();
while (advance() && peek() != '\n') {}
token.str = substr(token.index, index() - token.index);
token.type = sql_token_type::eol_comment;
emplace_token(token);
}
void pgsql_tokenizer::tokenize_eol_comment_or_operator_or_number()
{
auto n = next();
if (n == '-') {
tokenize_eol_comment();
return;
}
if (n == '>') { add_token(sql_token_type::binary_operator, next(2) == '>' ? 3 : 2);
return;
}
if (tokens_.empty() || current_token_type() != sql_token_type::number) {
auto number_str = extract_number();
if (!number_str.empty()) {
sql_token token;
token.index = index();
token.type = sql_token_type::number;
token.str = number_str;
advance(number_str.size() - 1);
emplace_token(token);
return;
}
}
add_token(sql_token_type::binary_operator);
}
void pgsql_tokenizer::tokenize_dollar_quoted_string()
{
sql_token token;
token.index = index();
token.type = sql_token_type::dollar_quoted_string;
while (advance()) {
auto c = peek();
if (!ddwaf::isalnum(c) && c != '_' && static_cast<uint8_t>(c) <= 0x7f) {
break;
}
}
if (peek() != '$') {
return;
}
auto tag = substr(token.index, index() - token.index + 1);
while (advance()) {
if (peek() == '$' && substr().starts_with(tag)) {
advance(tag.size() - 1);
break;
}
}
token.str = substr(token.index, index() - token.index + 1);
emplace_token(token);
}
void pgsql_tokenizer::tokenize_dollar_string_or_identifier()
{
auto n = next();
if (ddwaf::isalpha(n) || n == '_' || n == '$' || static_cast<uint8_t>(n) > 0x7f) {
tokenize_dollar_quoted_string();
} else {
auto str = substr();
std::string_view parameter;
const std::string_view ref(str.data(), str.size());
if (re2::RE2::PartialMatch(ref, *parameter_regex, ¶meter)) {
if (!parameter.empty()) {
add_token(sql_token_type::identifier, parameter.size());
}
}
}
}
std::vector<sql_token> pgsql_tokenizer::tokenize_impl()
{
for (; !eof(); advance()) {
auto c = peek();
if (ddwaf::isalpha(c) || c == '_' || static_cast<uint8_t>(c) > 0x7f) {
tokenize_string_keyword_operator_or_identifier();
} else if (ddwaf::isdigit(c)) {
tokenize_number();
} else if (c == '"') {
tokenize_conforming_string('"', sql_token_type::identifier);
} else if (c == '\'') {
tokenize_conforming_string('\'', sql_token_type::single_quoted_string);
} else if (c == '$') { tokenize_dollar_string_or_identifier();
} else if (c == '(') {
add_token(sql_token_type::parenthesis_open);
} else if (c == ')') {
add_token(sql_token_type::parenthesis_close);
} else if (c == '.') {
add_token(sql_token_type::dot);
} else if (c == ',') {
add_token(sql_token_type::comma);
} else if (c == '?') {
auto n = next();
if (n == '|' || n == '&') {
add_token(sql_token_type::binary_operator, 2);
} else {
add_token(sql_token_type::binary_operator);
}
} else if (c == '#') {
auto n = next();
if (n == '>') {
add_token(sql_token_type::binary_operator, next(2) == '>' ? 3 : 2);
} else if (n == '-') {
add_token(sql_token_type::binary_operator, 2);
} else {
add_token(sql_token_type::bitwise_operator);
}
} else if (c == '*') {
add_token(sql_token_type::asterisk);
} else if (c == ';') {
add_token(sql_token_type::query_end);
} else if (c == '/') {
tokenize_inline_comment_or_operator();
} else if (c == '-') {
tokenize_eol_comment_or_operator_or_number();
} else if (c == '@') {
auto n = next();
if (n == '@' || n == '>') {
add_token(sql_token_type::binary_operator, 2);
}
} else if (c == '!') {
add_token(sql_token_type::binary_operator, next() == '=' ? 2 : 1);
} else if (c == '>') {
auto n = next();
if (n == '>' || n == '=') {
add_token(sql_token_type::binary_operator, 2);
} else {
add_token(sql_token_type::binary_operator);
}
} else if (c == '<') {
auto n = next();
if (n == '=') {
add_token(sql_token_type::binary_operator, next(2) == '>' ? 3 : 2);
} else if (n == '@' || n == '<' || n == '>') {
add_token(sql_token_type::binary_operator, 2);
} else {
add_token(sql_token_type::binary_operator);
}
} else if (c == '=' || c == '%') {
add_token(sql_token_type::binary_operator);
} else if (c == '+') {
tokenize_operator_or_number();
} else if (c == '|') {
if (next() == '|') {
add_token(sql_token_type::binary_operator, 2);
} else {
add_token(sql_token_type::bitwise_operator);
}
} else if (c == '&' || c == '^' || c == '~') {
add_token(sql_token_type::bitwise_operator);
} else if (c == ':') {
auto n = next();
if (n == '=') {
add_token(sql_token_type::binary_operator, 2);
} else if (n == ':') {
add_token(sql_token_type::keyword, 2);
} else {
add_token(sql_token_type::colon);
}
} else if (c == '[') {
add_token(sql_token_type::array_open);
} else if (c == ']') {
add_token(sql_token_type::array_close);
} else if (!ddwaf::isspace(c)) {
DDWAF_DEBUG("Failed to parse sql statement {}, unexpected character {}", buffer_, c);
return {};
}
}
return tokens_;
}
}