#include <cstddef>
#include <cstdint>
#include <memory>
#include <ostream>
#include <stdexcept>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>
#include "re2.h"
#include "tokenizer/base.hpp"
#include "tokenizer/shell.hpp"
#include "utils.hpp"
using namespace std::literals;
namespace ddwaf {
namespace {
constexpr std::string_view redirection_regex_initialiser =
R"((&>>?|(?:[0-9]*>(?:\||>|&[0-9]*-?)?)|(?:[0-9]*<(?:<(?:-|<)?|&[0-9]*-?|>)?)))";
std::unique_ptr<re2::RE2> redirection_regex;
bool is_var_char(char c) { return ddwaf::isalnum(c) || c == '_'; }
bool is_field_char(char c)
{
return c != '`' && c != '$' && c != '{' && c != '}' && c != '[' && c != ']' && c != '(' &&
c != ')' && c != '\'' && c != '"' && c != '#' && c != '=' && c != '|' && c != '<' &&
c != '>' && c != ';' && c != '&' && c != '\n' && c != '\t' && c != ' ';
}
bool is_space_char(char c) { return c == ' ' || c == '\t'; }
void find_executables_and_strip_whitespaces(std::vector<shell_token> &tokens)
{
enum class command_scope : uint8_t {
variable_definition_or_executable,
variable_definition,
arguments,
none,
};
std::size_t read = 0;
std::size_t write = 0;
std::vector<command_scope> command_scope_stack{
command_scope::variable_definition_or_executable};
for (; read < tokens.size(); read++) {
auto &token = tokens[read];
auto &scope = command_scope_stack.back();
switch (token.type) {
case shell_token_type::backtick_substitution_open:
case shell_token_type::command_substitution_open:
case shell_token_type::process_substitution_open:
case shell_token_type::compound_command_open:
case shell_token_type::subshell_open:
if (scope == command_scope::variable_definition_or_executable) {
scope = command_scope::arguments;
}
command_scope_stack.emplace_back(command_scope::variable_definition_or_executable);
break;
case shell_token_type::backtick_substitution_close:
case shell_token_type::command_substitution_close:
case shell_token_type::process_substitution_close:
case shell_token_type::compound_command_close:
case shell_token_type::subshell_close:
case shell_token_type::arithmetic_expansion_close:
case shell_token_type::array_close:
case shell_token_type::file_redirection_close:
case shell_token_type::parameter_expansion_close:
command_scope_stack.pop_back();
break;
case shell_token_type::variable_definition:
if (scope == command_scope::variable_definition_or_executable) {
scope = command_scope::variable_definition;
}
break;
case shell_token_type::field:
case shell_token_type::variable:
case shell_token_type::single_quoted_string:
case shell_token_type::double_quoted_string:
if (scope == command_scope::variable_definition_or_executable) {
token.type = shell_token_type::executable;
scope = command_scope::arguments;
}
break;
case shell_token_type::whitespace:
if (scope == command_scope::variable_definition) {
scope = command_scope::variable_definition_or_executable;
}
continue;
case shell_token_type::control:
scope = command_scope::variable_definition_or_executable;
break;
case shell_token_type::arithmetic_expansion_open:
case shell_token_type::array_open:
case shell_token_type::file_redirection_open:
case shell_token_type::parameter_expansion_open:
command_scope_stack.emplace_back(command_scope::arguments);
break;
default:
break;
}
tokens[write++] = token;
}
tokens.resize(write);
}
}
std::ostream &operator<<(std::ostream &os, shell_token_type type)
{
switch (type) {
case shell_token_type::unknown:
os << "unknown";
break;
case shell_token_type::whitespace:
os << "whitespace";
break;
case shell_token_type::executable:
os << "executable";
break;
case shell_token_type::field:
os << "field";
break;
case shell_token_type::arithmetic_expansion:
os << "arithmetic_expansion";
break;
case shell_token_type::arithmetic_expansion_open:
os << "arithmetic_expansion_open";
break;
case shell_token_type::arithmetic_expansion_close:
os << "arithmetic_expansion_close";
break;
case shell_token_type::double_quoted_string_open:
os << "double_quoted_string_open";
break;
case shell_token_type::double_quoted_string_close:
os << "double_quoted_string_close";
break;
case shell_token_type::double_quoted_string:
os << "double_quoted_string";
break;
case shell_token_type::single_quoted_string:
os << "single_quoted_string";
break;
case shell_token_type::control:
os << "control";
break;
case shell_token_type::variable_definition:
os << "variable_definition";
break;
case shell_token_type::variable:
os << "variable";
break;
case shell_token_type::equal:
os << "equal";
break;
case shell_token_type::backtick_substitution_open:
os << "backtick_substitution_open";
break;
case shell_token_type::backtick_substitution_close:
os << "backtick_substitution_close";
break;
case shell_token_type::dollar:
os << "dollar";
break;
case shell_token_type::redirection:
os << "redirection";
break;
case shell_token_type::command_substitution_open:
os << "command_substitution_open";
break;
case shell_token_type::command_substitution_close:
os << "command_substitution_close";
break;
case shell_token_type::parenthesis_open:
os << "parenthesis_open";
break;
case shell_token_type::parenthesis_close:
os << "parenthesis_close";
break;
case shell_token_type::curly_brace_open:
os << "curly_brace_open";
break;
case shell_token_type::curly_brace_close:
os << "curly_brace_close";
break;
case shell_token_type::process_substitution_open:
os << "process_substitution_open";
break;
case shell_token_type::process_substitution_close:
os << "process_substitution_close";
break;
case shell_token_type::subshell_open:
os << "subshell_open";
break;
case shell_token_type::subshell_close:
os << "subshell_close";
break;
case shell_token_type::compound_command_open:
os << "compound_command_open";
break;
case shell_token_type::compound_command_close:
os << "compound_command_close";
break;
case shell_token_type::array_open:
os << "array_open";
break;
case shell_token_type::array_close:
os << "array_close";
break;
case shell_token_type::parameter_expansion_open:
os << "parameter_expansion_open";
break;
case shell_token_type::parameter_expansion_close:
os << "parameter_expansion_close";
break;
case shell_token_type::file_redirection_open:
os << "file_redirection_open";
break;
case shell_token_type::file_redirection_close:
os << "file_redirection_close";
break;
}
return os;
}
bool shell_tokenizer::initialise_regexes()
{
static const bool ret = []() {
try {
redirection_regex = std::make_unique<re2::RE2>(redirection_regex_initialiser);
return redirection_regex->ok();
} catch (...) {
return false;
}
}();
return ret;
}
shell_tokenizer::shell_tokenizer(
std::string_view str, std::unordered_set<shell_token_type> skip_tokens)
: base_tokenizer(str, std::move(skip_tokens))
{
shell_scope_stack_.reserve(8);
if (!initialise_regexes()) {
throw std::runtime_error("redirection regex not valid: " + redirection_regex->error_arg());
}
}
void shell_tokenizer::tokenize_delimited_token(std::string_view delimiter, shell_token_type type)
{
shell_token token;
token.index = index();
token.type = type;
std::size_t idx = 0;
while (idx < delimiter.size() && advance()) { idx = (peek() == delimiter[idx] ? idx + 1 : 0); }
token.str = substr(token.index, index() - token.index + 1);
emplace_token(token);
}
void shell_tokenizer::tokenize_variable()
{
shell_token token;
token.index = index();
token.type = shell_token_type::variable;
advance();
auto c = peek();
if (c == '-' || c == '?' || c == '@' || c == '#' || c == '*' || c == '$' || c == '!' ||
ddwaf::isdigit(c)) {
token.str = substr(token.index, 2);
} else { while (is_var_char(next()) && advance()) {};
token.str = substr(token.index, index() - token.index + 1);
}
emplace_token(token);
}
void shell_tokenizer::tokenize_expandable_scope(std::string_view delimiter,
shell_token_type full_token, shell_token_type open_token, shell_token_type close_token)
{
unsigned slash_count = 0;
std::size_t delim_idx = 0;
for (; !eof(); advance()) {
auto c = peek();
if (delim_idx < delimiter.size() && c == delimiter[delim_idx] && slash_count == 0) {
++delim_idx;
if (delim_idx == delimiter.size()) {
pop_shell_scope();
auto &token = current_token();
if (token.type == open_token) {
token.type = full_token;
token.str = substr(token.index, index() - token.index + 1);
} else {
add_token(close_token);
}
break;
}
} else {
delim_idx = 0;
}
if (c == '$' && slash_count == 0) {
auto n = next();
if (n == '(') {
auto n2 = next(2);
if (n2 == '(') {
add_token(shell_token_type::arithmetic_expansion_open, 3);
push_shell_scope(shell_scope::arithmetic_expansion);
} else if (n2 == '<') {
add_token(shell_token_type::file_redirection_open, 3);
push_shell_scope(shell_scope::file_redirection);
} else {
add_token(shell_token_type::command_substitution_open, 2);
push_shell_scope(shell_scope::command_substitution);
}
break;
}
if (n == '[') {
add_token(shell_token_type::arithmetic_expansion_open, 2);
push_shell_scope(shell_scope::legacy_arithmetic_expansion);
break;
}
if (n == '{') {
add_token(shell_token_type::parameter_expansion_open, 2);
push_shell_scope(shell_scope::parameter_expansion);
break;
}
} else if (c == '`') {
add_token(shell_token_type::backtick_substitution_open);
push_shell_scope(shell_scope::backtick_substitution);
break;
} else if (c == '\\') {
slash_count ^= 1;
}
}
}
void shell_tokenizer::tokenize_field(shell_token_type type)
{
shell_token token;
token.index = index();
token.type = type;
while (is_field_char(next()) && advance()) {}
token.str = substr(token.index, index() - token.index + 1);
emplace_token(token);
}
void shell_tokenizer::tokenize_redirection()
{
shell_token token;
token.index = index();
token.type = shell_token_type::redirection;
auto remaining_str = substr(index());
std::string_view redirection;
const std::string_view ref(remaining_str.data(), remaining_str.size());
if (re2::RE2::PartialMatch(ref, *redirection_regex, &redirection)) {
if (!redirection.empty()) {
token.str = substr(token.index, redirection.size());
advance(token.str.size() - 1);
emplace_token(token);
}
}
}
void shell_tokenizer::tokenize_redirection_or_field()
{
shell_token token;
token.index = index();
auto n = next();
while (ddwaf::isdigit(n) && advance()) { n = next(); }
if (n == '>' || n == '<') {
advance(); auto remaining_str = substr(index());
std::string_view redirection;
const std::string_view ref(remaining_str.data(), remaining_str.size());
if (re2::RE2::PartialMatch(ref, *redirection_regex, &redirection)) {
if (!redirection.empty()) {
token.type = shell_token_type::redirection;
token.str = substr(token.index, index() - token.index + redirection.size());
advance(redirection.size());
emplace_token(token);
}
}
} else {
token.type = shell_token_type::field;
while (is_field_char(n) && advance()) { n = next(); }
token.str = substr(token.index, index() - token.index + 1);
emplace_token(token);
}
}
std::vector<shell_token> shell_tokenizer::tokenize()
{
push_shell_scope(shell_scope::global);
for (; !eof(); advance()) {
if (current_shell_scope_ == shell_scope::double_quoted_string) {
tokenize_expandable_scope("\"", shell_token_type::double_quoted_string,
shell_token_type::double_quoted_string_open,
shell_token_type::double_quoted_string_close);
continue;
}
if (current_shell_scope_ == shell_scope::parameter_expansion) {
tokenize_expandable_scope("}", shell_token_type::variable,
shell_token_type::parameter_expansion_open,
shell_token_type::parameter_expansion_close);
continue;
}
if (current_shell_scope_ == shell_scope::arithmetic_expansion) {
tokenize_expandable_scope("))", shell_token_type::arithmetic_expansion,
shell_token_type::arithmetic_expansion_open,
shell_token_type::arithmetic_expansion_close);
continue;
}
if (current_shell_scope_ == shell_scope::legacy_arithmetic_expansion) {
tokenize_expandable_scope("]", shell_token_type::arithmetic_expansion,
shell_token_type::arithmetic_expansion_open,
shell_token_type::arithmetic_expansion_close);
continue;
}
if (current_shell_scope_ == shell_scope::array) {
tokenize_expandable_scope(")", shell_token_type::field, shell_token_type::array_open,
shell_token_type::array_close);
continue;
}
if (current_shell_scope_ == shell_scope::file_redirection) {
tokenize_expandable_scope(")", shell_token_type::redirection,
shell_token_type::file_redirection_open, shell_token_type::file_redirection_close);
continue;
}
auto c = peek();
if (is_space_char(c)) {
shell_token token;
token.index = index();
token.type = shell_token_type::whitespace;
while (is_space_char(next()) && advance()) {}
token.str = substr(token.index, index() - token.index + 1);
emplace_token(token);
} else if (c == '"') {
add_token(shell_token_type::double_quoted_string_open);
push_shell_scope(shell_scope::double_quoted_string);
} else if (c == '$') {
auto n = next();
if (n == '(') {
auto n2 = next(2);
if (n2 == '(') {
add_token(shell_token_type::arithmetic_expansion_open, 3);
push_shell_scope(shell_scope::arithmetic_expansion);
} else if (n2 == '<') {
add_token(shell_token_type::file_redirection_open, 3);
push_shell_scope(shell_scope::file_redirection);
} else {
add_token(shell_token_type::command_substitution_open, 2);
push_shell_scope(shell_scope::command_substitution);
}
} else if (ddwaf::isalnum(n) || n == '_' || n == '-' || n == '?' || n == '@' ||
n == '#' || n == '*' || n == '$' || n == '!') {
tokenize_variable();
} else if (n == '{') {
add_token(shell_token_type::parameter_expansion_open, 2);
push_shell_scope(shell_scope::parameter_expansion);
} else if (n == '[') {
add_token(shell_token_type::arithmetic_expansion_open, 2);
push_shell_scope(shell_scope::legacy_arithmetic_expansion);
}
} else if (c == ')') {
if (current_shell_scope_ == shell_scope::command_substitution) {
add_token(shell_token_type::command_substitution_close);
pop_shell_scope();
} else if (current_shell_scope_ == shell_scope::process_substitution) {
add_token(shell_token_type::process_substitution_close);
pop_shell_scope();
} else if (current_shell_scope_ == shell_scope::subshell) {
add_token(shell_token_type::subshell_close);
pop_shell_scope();
} else {
add_token(shell_token_type::parenthesis_close);
}
} else if (c == '`') {
if (current_shell_scope_ == shell_scope::backtick_substitution) {
add_token(shell_token_type::backtick_substitution_close);
pop_shell_scope();
} else {
add_token(shell_token_type::backtick_substitution_open);
push_shell_scope(shell_scope::backtick_substitution);
}
} else if (c == '(') {
if (!tokens_.empty() && current_token_type() == shell_token_type::equal) {
add_token(shell_token_type::array_open);
push_shell_scope(shell_scope::array);
} else if (next() == '(') {
add_token(shell_token_type::arithmetic_expansion_open, 2);
push_shell_scope(shell_scope::arithmetic_expansion);
} else if (is_beginning_of_command()) {
add_token(shell_token_type::subshell_open);
push_shell_scope(shell_scope::subshell);
} else {
add_token(shell_token_type::parenthesis_open);
}
} else if (c == '=') {
add_token(shell_token_type::equal);
} else if (c == '\n' || c == ';' || c == '!') {
add_token(shell_token_type::control);
} else if (c == '|') {
add_token(shell_token_type::control, next() == '|' ? 2 : 1);
} else if (c == '&') {
auto n = next();
if (n == '>') {
tokenize_redirection();
} else if (n == '&') {
add_token(shell_token_type::control, 2);
} else {
add_token(shell_token_type::control);
}
} else if (c == '{') {
auto n = next();
if (n == ' ' && is_beginning_of_command()) {
add_token(shell_token_type::compound_command_open);
advance();
push_shell_scope(shell_scope::compound_command);
} else {
add_token(shell_token_type::curly_brace_open);
}
} else if (c == '}') {
if (current_shell_scope_ == shell_scope::compound_command &&
match_last_nonws_token_with_one_of(";"sv, "\n")) {
add_token(shell_token_type::compound_command_close);
pop_shell_scope();
} else {
add_token(shell_token_type::curly_brace_close);
}
} else if (c == '\'') {
tokenize_delimited_token("'", shell_token_type::single_quoted_string);
} else if (ddwaf::isdigit(c)) {
tokenize_redirection_or_field();
} else if (c == '<' || c == '>') {
auto n = next();
if (n == '(') {
add_token(shell_token_type::process_substitution_open, 2);
push_shell_scope(shell_scope::process_substitution);
} else {
tokenize_redirection();
}
} else {
tokenize_field();
if (next() == '=') {
current_token().type = shell_token_type::variable_definition;
}
}
}
find_executables_and_strip_whitespaces(tokens_);
return tokens_;
}
}