// SPDX-FileCopyrightText: 2020 ret2libc // SPDX-License-Identifier: LGPL-3.0-only const SPECIAL_CHARACTERS = ["\\s", "@", "|", "#", '"', "'", ">", ";", "$", "`", "~", "\\", ",", "(", ")"]; const SPEC_SPECIAL_CHARACTERS = ["\\s", "@", "|", "#", '"', "'", ">", ";", "$", "`", "~", "\\", ",", "(", ")", ":"]; const SPECIAL_CHARACTERS_EQUAL = SPECIAL_CHARACTERS.concat(["="]); const SPECIAL_CHARACTERS_COMMA = SPECIAL_CHARACTERS.concat([","]); const ARG_IDENTIFIER_BASE = choice( repeat1(noneOf(...SPECIAL_CHARACTERS)), "$$$", "$$", /\$[^\s@|#"'>;`~\\({) ]/, /\$\{[^\r\n $}]+\}/, /\\./, ); const SPEC_ARG_IDENTIFIER_BASE = choice( repeat1(noneOf(...SPEC_SPECIAL_CHARACTERS)), "$$$", "$$", /\$[^\s@|#"'>;`~\\({) ]/, /\$\{[^\r\n $}]+\}/, /\\./, ); module.exports = grammar({ name: "rzcmd", extras: ($) => [$._comment, /[ \t]*/], externals: ($) => [$._cmd_identifier, $._help_stmt, $.file_descriptor, $._eq_sep_concat, $._concat, $._spec_sep], inline: ($) => [$.stmt_delimiter, $.stmt_delimiter_singleline], rules: { statements: ($) => choice( seq(), seq(repeat($.stmt_delimiter)), seq(repeat($.stmt_delimiter), $._statement, repeat(seq($.stmt_delimiter, optional($._statement)))), ), _statements_singleline: ($) => prec( 1, seq( repeat($.stmt_delimiter_singleline), $._statement, repeat(seq($.stmt_delimiter_singleline, optional($._statement))), ), ), _statement: ($) => choice($.redirect_stmt, $._simple_stmt), _simple_stmt: ($) => choice( $.help_stmt, $.repeat_stmt, $.arged_stmt, $.macro_stmt, $.tmp_stmt, $._iter_stmt, $._pipe_stmt, $.grep_stmt, ), tmp_stmt: ($) => prec.right(seq($._simple_stmt, repeat1($._tmp_op))), _tmp_op: ($) => choice( $.tmp_seek_op, $.tmp_blksz_op, $.tmp_fromto_op, $.tmp_arch_op, $.tmp_bits_op, $.tmp_nthi_op, $.tmp_eval_op, $.tmp_fs_op, $.tmp_reli_op, $.tmp_kuery_op, $.tmp_fd_op, $.tmp_reg_op, $.tmp_file_op, $.tmp_string_op, $.tmp_value_op, $.tmp_hex_op, ), _iter_stmt: ($) => choice( $.iter_file_lines_stmt, $.iter_offsets_stmt, $.iter_offsetssizes_stmt, $.iter_hit_stmt, $.iter_interpret_stmt, $.iter_interpret_offsetssizes_stmt, $.iter_comment_stmt, $.iter_dbta_stmt, $.iter_dbtb_stmt, $.iter_dbts_stmt, $.iter_threads_stmt, $.iter_bbs_stmt, $.iter_instrs_stmt, $.iter_import_stmt, $.iter_sections_stmt, $.iter_segments_stmt, $.iter_symbol_stmt, $.iter_string_stmt, $.iter_flags_stmt, $.iter_function_stmt, $.iter_iomap_stmt, $.iter_dbgmap_stmt, $.iter_register_stmt, $.iter_step_stmt, ), _pipe_stmt: ($) => choice($.html_disable_stmt, $.html_enable_stmt, $.pipe_stmt), grep_stmt: ($) => seq(field("command", $._simple_stmt), "~", field("specifier", $.grep_specifier)), // FIXME: improve parser for grep specifier // grep_specifier_identifier also includes ~ because r2 does not support nested grep statements yet grep_specifier_identifier: ($) => token(seq(repeat1(choice(/[^\n\r;#@>|`$()]+/, /\\./, /\$[^(\r\n;#>|`]/)))), grep_specifier: ($) => prec.left( choice( seq( repeat1(choice($.grep_specifier_identifier, $.cmd_substitution_arg)), optional(alias(/[$]+/, $.grep_specifier_identifier)), ), alias(/[$]+/, $.grep_specifier_identifier), ), ), html_disable_stmt: ($) => prec.right(1, seq(field("command", $._simple_stmt), "|")), html_enable_stmt: ($) => prec.right(1, seq(field("command", $._simple_stmt), "|H")), pipe_stmt: ($) => seq($._simple_stmt, "|", $.args), pipe_second_stmt: ($) => /[^|\r\n;]+/, iter_file_lines_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@.", $.arg)), iter_offsets_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@=", optional($.args))), iter_offsetssizes_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@@=", optional($.args))), iter_hit_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@", $._concat, alias($._search_stmt, $.arged_stmt))), iter_interpret_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@c:", $._simple_stmt)), iter_interpret_offsetssizes_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@@c:", $._simple_stmt)), iter_comment_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@C", optional(seq(":", $.arg)))), iter_dbta_stmt: ($) => prec.right(1, seq($._simple_stmt, choice("@@dbt", "@@dbta"))), iter_dbtb_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@dbtb")), iter_dbts_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@dbts")), iter_threads_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@t")), iter_bbs_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@b")), iter_instrs_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@i")), iter_import_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@ii")), iter_sections_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@iS")), iter_segments_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@iSS")), iter_symbol_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@is")), iter_string_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@iz")), iter_flags_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@f", optional(seq(":", $.arg)))), iter_function_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@F", optional(seq(":", $.arg)))), iter_iomap_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@om")), iter_dbgmap_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@dm")), iter_register_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@r")), iter_step_stmt: ($) => prec.right(1, seq($._simple_stmt, "@@s:", $.args)), // tmp changes statements tmp_seek_op: ($) => seq("@ ", $.args), tmp_blksz_op: ($) => seq("@!", $.args), tmp_fromto_op: ($) => seq("@(", $.args, ")"), tmp_arch_op: ($) => seq("@a:", $.arg), tmp_bits_op: ($) => seq("@b:", $.args), tmp_nthi_op: ($) => seq("@B:", $.arg), tmp_eval_op: ($) => seq("@e:", alias($.tmp_eval_args, $.args)), tmp_fs_op: ($) => seq("@F:", $.arg), tmp_reli_op: ($) => seq("@i:", $.args), tmp_kuery_op: ($) => seq("@k:", $.arg), tmp_fd_op: ($) => seq("@o:", $.args), tmp_reg_op: ($) => seq("@r:", $.arg), tmp_file_op: ($) => seq("@f:", $.arg), tmp_string_op: ($) => seq("@s:", $.arg), tmp_value_op: ($) => seq("@v:", $.arg), tmp_hex_op: ($) => seq("@x:", $.arg), // basic statements help_stmt: ($) => prec.left( 1, choice( field("command", alias($.question_mark_identifier, $.cmd_identifier)), field("command", alias(choice("?**", "?**e", "?***"), $.cmd_identifier)), field("command", alias($._help_stmt, $.cmd_identifier)), ), ), macro_body: ($) => seq(";", $._statement, repeat(seq(";", $._statement))), macro_name: ($) => /[A-Za-z0-9_\-\.]+/, macro_content: ($) => prec(1, seq(field("name", $.macro_name), field("args", optional($.args)), field("body", $.macro_body))), macro_call: ($) => seq("(", field("argv", optional($.args)), ")"), macro_stmt: ($) => seq(field("command", alias("(", $.cmd_identifier)), $._concat, $.macro_content, ")", optional($.macro_call)), arged_stmt: ($) => choice( $._simple_arged_stmt, $._pointer_arged_stmt, $._macro_arged_stmt, $._alias_arged_stmt, $._system_stmt, $._interpret_stmt, $._env_stmt, $._last_stmt, $._simple_arged_stmt_question, ), _macro_arged_stmt: ($) => choice( field("command", alias(choice("(", "(-*", "(*"), $.cmd_identifier)), seq(field("command", alias("(-", $.cmd_identifier)), field("args", $.args)), ), _alias_arged_stmt: ($) => prec.left( 1, choice( field("command", alias(choice("$*", "$**"), $.cmd_identifier)), seq(field("command", alias("$", $.cmd_identifier)), optional(field("args", $.args))), ), ), _simple_arged_stmt_question: ($) => prec.left(1, seq(field("command", alias($._help_stmt, $.cmd_identifier)), field("args", $.args))), _simple_arged_stmt: ($) => prec.left(1, seq(field("command", $.cmd_identifier), field("args", optional($.args)))), _search_stmt: ($) => prec.left( 1, seq(field("command", alias(/\/[A-Za-z0-9+!\/*]*/, $.cmd_identifier)), field("args", optional($.args))), ), _pointer_arged_stmt: ($) => prec.left( 1, seq( field("command", alias($.pointer_identifier, $.cmd_identifier)), field("args", alias($.eq_sep_args, $.args)), ), ), _system_stmt: ($) => prec.left(1, seq(field("command", $.system_identifier), optional(field("args", $.args)))), _interpret_stmt: ($) => prec.left( 1, choice( seq(field("command", alias(".", $.cmd_identifier)), field("args", $._simple_stmt)), seq( field("command", alias(/\.[\.:\-*]+/, $.cmd_identifier)), optional(seq(/[ ]+/, field("args", optional($.args)))), ), seq(field("command", alias(/\.[ ]+/, $.cmd_identifier)), field("args", optional($.args))), seq(field("command", alias(/\.\.?\(/, $.cmd_identifier)), field("args", optional($.args)), ")"), seq(field("command", alias($._interpret_search_identifier, $.cmd_identifier)), field("args", $.args)), prec.right(1, seq(field("args", $._simple_stmt), field("command", "|."))), ), ), _interpret_search_identifier: ($) => "./", _env_stmt: ($) => prec.left( seq( field("command", alias($._env_stmt_identifier, $.cmd_identifier)), optional(seq(/[ ]+/, field("args", optional(alias($.eq_sep_args, $.args))))), ), ), _env_stmt_identifier: ($) => "env", _last_stmt: ($) => seq(field("command", alias($.last_stmt_identifier, $.cmd_identifier))), last_stmt_identifier: ($) => choice(".", "..."), interpret_arg: ($) => $._any_stmt, system_identifier: ($) => /![\*!-=]*/, question_mark_identifier: ($) => "?", repeat_stmt: ($) => prec.left(1, seq(field("arg", alias($._dec_number, $.number)), field("command", $._simple_stmt))), pointer_identifier: ($) => "*", eq_sep_args: ($) => seq(alias($._eq_sep_key, $.arg), optional(seq("=", alias($._eq_sep_val, $.arg)))), redirect_stmt: ($) => prec.right( 2, seq(field("command", $._simple_stmt), field("redirect_operator", $._redirect_operator), field("arg", $.arg)), ), _redirect_operator: ($) => choice($.fdn_redirect_operator, $.fdn_append_operator, $.html_redirect_operator, $.html_append_operator), fdn_redirect_operator: ($) => seq(optional($.file_descriptor), ">"), fdn_append_operator: ($) => seq(optional($.file_descriptor), ">>"), html_redirect_operator: ($) => "H>", html_append_operator: ($) => "H>>", _arg_with_paren: ($) => seq(alias("(", $.arg_identifier), $.args, alias(")", $.arg_identifier)), _arg: ($) => choice( $.arg_identifier, $.double_quoted_arg, $.single_quoted_arg, $.cmd_substitution_arg, alias($._arg_with_paren, $.args), alias(",", $.arg_identifier), ), arg: ($) => choice($._arg, $.concatenation), args: ($) => prec.left(repeat1($.arg)), // TODO: this should accept a quoted_arg and a cmd_substitution_arg as well tmp_eval_args: ($) => prec.left(seq(alias($.tmp_eval_arg, $.arg), repeat(seq(",", alias($.tmp_eval_arg, $.arg))))), tmp_eval_arg: ($) => alias(repeat1(noneOf(...SPECIAL_CHARACTERS_COMMA)), $.arg_identifier), _eq_sep_key_single: ($) => choice( alias($._eq_sep_key_identifier, $.arg_identifier), $.double_quoted_arg, $.single_quoted_arg, $.cmd_substitution_arg, ), _eq_sep_key_concatenation: ($) => prec.left(seq($._eq_sep_key_single, repeat1(seq($._eq_sep_concat, $._eq_sep_key_single)))), _eq_sep_key: ($) => choice($._eq_sep_key_single, alias($._eq_sep_key_concatenation, $.concatenation)), _eq_sep_key_identifier: ($) => token( repeat1( choice( repeat1(noneOf(...SPECIAL_CHARACTERS_EQUAL)), /\$[^({]/, /\$\{[^\r\n $}]+\}/, escape(...SPECIAL_CHARACTERS_EQUAL), ), ), ), _eq_sep_val_concatenation: ($) => prec.left(1, seq($.arg, repeat1(seq($._eq_sep_concat, $.arg)))), _eq_sep_val: ($) => choice($._arg, alias($._eq_sep_val_concatenation, $.concatenation)), _any_stmt: ($) => /[^\r\n;~|]+/, arg_identifier: ($) => argIdentifier(ARG_IDENTIFIER_BASE), spec_arg_identifier: ($) => argIdentifier(SPEC_ARG_IDENTIFIER_BASE), double_quoted_arg: ($) => seq( '"', repeat(choice(token.immediate(prec(1, /[^\\"\n$`]+/)), /\$[^("]?/, /\\[\\"\n$`]?/, $.cmd_substitution_arg)), '"', ), single_quoted_arg: ($) => seq("'", repeat(choice(token.immediate(prec(1, /[^\\'\n]+/)), /\\[\\'\n]?/)), "'"), cmd_substitution_arg: ($) => choice(seq("$(", $._statements_singleline, ")"), prec(1, seq("`", $._statements_singleline, "`"))), concatenation: ($) => prec(-1, seq($._arg, repeat1(prec(-1, seq($._concat, $._arg))))), _dec_number: ($) => choice(/[1-9][0-9]*/, /[0-9][0-9]+/), _comment: ($) => /#[^\r\n]*/, stmt_delimiter: ($) => choice("\n", "\r", $.stmt_delimiter_singleline), stmt_delimiter_singleline: ($) => ";", specifiers: ($) => repeat1(seq($._spec_sep, $._concat, alias($.spec_arg_identifier, $.arg_identifier))), cmd_identifier: ($) => seq(field("id", $._cmd_identifier), field("extra", optional($.specifiers))), }, }); function noneOf(...characters) { const negatedString = characters.map((c) => (c == "\\" ? "\\\\" : c)).join(""); return new RegExp("[^" + negatedString + "]"); } function argIdentifier(baseCharacters) { return choice(token(repeat1(baseCharacters)), "$"); }