#!/usr/bin/env python3 """ PM3 Help 2 List This script takes the full text help output from the PM3 client and converts it to a list to be used for readline autocomplete. It is based on pm3_help2JSON.py by Original Authors / Maintainers: - Samuel Windall This version - Iceman Note: This file is used as a helper script to generate the rl_vocabulory.h file need. It also needs a working proxmark3 client to extract the help text. Ie: this script can't be used inside the normal build sequence. """ import re import datetime import argparse import logging ############################################################################## # Script version data: (Please increment when making updates) APP_NAME = 'PM3Help2List' VERSION_MAJOR = 1 VERSION_MINOR = 0 ############################################################################## # Main Application Code: def main(): """The main function for the script""" args = build_arg_parser().parse_args() logging_format = '%(message)s' if args.debug: logging.basicConfig(level=logging.DEBUG, format=logging_format) else: logging.basicConfig(level=logging.WARN, format=logging_format) logging.info(f'{get_version()} starting...') help_text = args.input_file.read() command_data = parse_all_command_data(help_text) args.output_file.write("""//----------------------------------------------------------------------------- // Copyright (C) 2021 // // This code is licensed to you under the terms of the GNU GPL, version 2 or, // at your option, any later version. See the LICENSE.txt file for the text of // the license. //----------------------------------------------------------------------------- // readline auto complete utilities //----------------------------------------------------------------------------- #ifndef RL_VOCABULORY_H__ #define RL_VOCABULORY_H__ #ifdef __cplusplus extern "C" { #endif #ifdef HAVE_READLINE #include #include #include #include "ui.h" // g_session char* rl_command_generator(const char *text, int state); char **rl_command_completion(const char *text, int start, int end); typedef struct vocabulory_s { bool offline; const char *name; } vocabulory_t; const static vocabulory_t vocabulory[] = {\n""") for key, values in command_data.items(): offline = 0 if (values['offline'] == True): offline = 1 cmd = values['command'] args.output_file.write(' {{ {}, "{}" }}, \n'.format(offline, cmd)) args.output_file.write(""" {0, NULL}\n}; char **rl_command_completion(const char *text, int start, int end) { rl_attempted_completion_over = 1; return rl_completion_matches (text, rl_command_generator); } char* rl_command_generator(const char *text, int state) { static int index; static size_t len; size_t rlen = strlen(rl_line_buffer); const char *command; if (!state) { index = 0; len = strlen(text); } while ((command = vocabulory[index].name)) { // When no pm3 device present // and the command is not available offline, // we skip it. if ((g_session.pm3_present == false) && (vocabulory[index].offline == false )) { index++; continue; } index++; if (strncmp (command, rl_line_buffer, rlen) == 0) { const char *next = command + (rlen - len); const char *space = strstr(next, " "); if (space != NULL) { return strndup(next, space - next); } return strdup(next); } } return NULL; } #endif #ifdef __cplusplus } #endif #endif""") logging.info(f'{get_version()} completed!') def build_arg_parser(): """Build the argument parser for reading the program arguments""" parser = argparse.ArgumentParser() parser.add_argument('input_file', type=argparse.FileType('r'), help='Source of full text help from the PM3 client.') parser.add_argument('output_file', type=argparse.FileType('w'), help='Destination for list output.') parser.add_argument('--version', '-v', action='version', version=get_version(), help='Version data about this app.') parser.add_argument('--debug', '-d', action='store_true', help='Log debug messages.') return parser def build_help_regex(): """The regex uses to parse the full text output of help data from the pm3 client.""" # Reads the divider followed by the command itself re_command = r'-{87}\n(?P.+)\n' # Reads if the command is available offline re_offline = r'available offline: (?Pyes|no)\n+' return re.compile(re_command+re_offline, re.MULTILINE); def parse_all_command_data(help_text): """Turns the full text output of help data from the pm3 client into a list of dictionaries""" command_dicts = {} # Strip out ANSI escape sequences help_text = remove_ansi_escape_codes(help_text) # Find all commands in the full text help output matches = build_help_regex().finditer(help_text) for match in matches: # Turn a match into a dictionary with keys for the extracted fields command_object = parse_command_data(match) # Store this command against its name for easy lookup command_dicts[command_object['command']] = command_object return command_dicts def parse_command_data(match): """Turns a regex match of a command in the help text and converts it into a dictionary""" logging.info('Parsing new command...') # Get and clean the command string command = remove_extra_whitespace(match.group('command')) logging.info(f' Command: {command}') # Get the online status as a boolean. Note: the regex only picks up 'yes' or 'no' so this check is safe. offline = (match.group('offline') == 'yes') logging.debug(f' Offline: {offline}') # Construct the command dictionary command_data = { 'command': command, 'offline': offline, } logging.info('Completed parsing command!') return command_data ############################################################################## # Helper Functions: def get_version(): """Get the version string for this script""" return f'{APP_NAME} v{VERSION_MAJOR}.{VERSION_MINOR:02}' def remove_ansi_escape_codes(text): """Remove ANSI escape sequences that may be left in the text.""" re_ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') return re_ansi_escape.sub('', str(text)).lower() def remove_extra_whitespace(text): """Removes extra whitespace that may be in the text.""" # Ensure input is a string text = str(text) # Remove whitespace from the start and end of the text text = text.strip() # Deduplicate spaces in the string text = re.sub(r' +', ' ', text) return text def text_to_oneliner(text): """Converts a multi line string into a single line string and removes extra whitespace""" # Ensure input is a string text = str(text) # Replace newlines with spaces text = re.sub(r'\n+', ' ', text) # Remove the extra whitespace text = remove_extra_whitespace(text) return text def text_to_list(text): """Converts a multi line string into a list of lines and removes extra whitespace""" # Ensure input is a string text = str(text) # Get all the lines lines = text.strip().split('\n') # For each line clean up any extra whitespace return [remove_extra_whitespace(line) for line in lines] ############################################################################## # Application entrypoint: if __name__ == '__main__': main()