forked from RfidResearchGroup/proxmark3
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpm3_help2json.py
executable file
·207 lines (172 loc) · 7.45 KB
/
pm3_help2json.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#!/usr/bin/env python3
"""
PM3 Help 2 JSON
This script takes the full text help output from the PM3 client and converts it to JSON.
Authors / Maintainers:
- Samuel Windall
Note:
This file is used during the pm3 client build
any changes to the call script parameters should be reflected in the makefile
"""
import re
import json
import datetime
import argparse
import logging
##############################################################################
# Script version data: (Please increment when making updates)
APP_NAME = 'PM3Help2JSON'
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)
meta_data = build_metadata(args.meta, command_data)
output_data = {
'metadata': meta_data,
'commands': command_data,
}
json.dump(output_data, args.output_file, indent=4, sort_keys=True)
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 JSON output.')
parser.add_argument('--meta', action='append', help='Additional metadata to be included.', metavar='key:value')
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<command>.+)\n'
# Reads if the command is available offline
re_offline = r'available offline: (?P<offline>yes|no)\n+'
# Reads the description lines
re_description = r'(?P<description>(.|\n)+?)\n+'
# Reads the usage string
re_usage = r'usage:\n(?P<usage>(?:.+\n)+)\n+'
# Reads the options and there individual descriptions
re_options = r'options:\n(?P<options>(?:.+\n)+)\n'
# Reads the notes and examples
re_notes = r'examples\/notes:\n(?P<notes>(?:.+\n)+)'
# Combine them into a single regex object
re_full = re.compile(re_command+re_offline+re_description+re_usage+re_options+re_notes, re.MULTILINE);
return re_full
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}')
# Get and clean the description paragraph
description = text_to_oneliner(match.group('description'))
logging.debug(f' Description: {description}')
# Get and clen the usage string
usage = text_to_oneliner(match.group('usage'))
logging.debug(f' Usage: {usage}')
# Get and clean the list of options
options = text_to_list(match.group('options'))
logging.debug(f' Options: {options}')
# Get and clean the list of examples and notes
notes = text_to_list(match.group('notes'))
logging.debug(f' Notes: {notes}')
# Construct the command dictionary
command_data = {
'command': command,
'offline': offline,
'description': description,
'usage': usage,
'options': options,
'notes': notes
}
logging.info('Completed parsing command!')
return command_data
def build_metadata(extra_data, command_data):
"""Turns the full text output of help data from the pm3 client into a list of dictionaries."""
logging.info('Building metadata...')
metadata = {
'extracted_by': get_version(),
'extracted_on': datetime.datetime.utcnow().replace(microsecond=0).isoformat(),
'commands_extracted': len(command_data)
}
for key, value in metadata.items():
logging.debug(f' {key} - {value}')
if extra_data:
for extra in extra_data:
parts = extra.split(':')
if len(parts) == 2:
metadata[parts[0]] = parts[1]
logging.debug(f' {parts[0]} - {parts[1]}')
else:
logging.warning(f'Error building metadata. '
f'Skipped "{extra}". '
f'Extra metadata must be in the format "key:value".')
logging.info('Completed building metadata!')
return metadata
##############################################################################
# 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()