From 11d6f78e43e0f305be43aeb8fc75d9d6733e508a Mon Sep 17 00:00:00 2001 From: Jordan Wiens Date: Tue, 13 Sep 2016 18:24:02 -0400 Subject: rename files to allow them to be imported --- python/examples/bin-info.py | 34 ------- python/examples/bin_info.py | 34 +++++++ python/examples/export-svg.py | 166 -------------------------------- python/examples/export_svg.py | 166 ++++++++++++++++++++++++++++++++ python/examples/instruction-iterator.py | 49 ---------- python/examples/instruction_iterator.py | 49 ++++++++++ python/examples/jump-table.py | 63 ------------ python/examples/jump_table.py | 63 ++++++++++++ python/examples/version-switcher.py | 119 ----------------------- python/examples/version_switcher.py | 119 +++++++++++++++++++++++ 10 files changed, 431 insertions(+), 431 deletions(-) delete mode 100644 python/examples/bin-info.py create mode 100644 python/examples/bin_info.py delete mode 100755 python/examples/export-svg.py create mode 100755 python/examples/export_svg.py delete mode 100644 python/examples/instruction-iterator.py create mode 100644 python/examples/instruction_iterator.py delete mode 100644 python/examples/jump-table.py create mode 100644 python/examples/jump_table.py delete mode 100644 python/examples/version-switcher.py create mode 100644 python/examples/version_switcher.py (limited to 'python/examples') diff --git a/python/examples/bin-info.py b/python/examples/bin-info.py deleted file mode 100644 index 48073894..00000000 --- a/python/examples/bin-info.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -import sys, binaryninja, time -if sys.platform.lower().startswith("linux"): - bintype="ELF" -elif sys.platform.lower() == "darwin": - bintype="Mach-O" -else: - raise Exception, "%s is not supported on this plugin" % sys.platform - -if len(sys.argv) > 1: - target = sys.argv[1] -else: - target = "/bin/ls" - -bv = binaryninja.BinaryViewType[bintype].open(target) -bv.update_analysis_and_wait() - -print "-------- %s --------" % target -print "START: 0x%x" % bv.start -print "ENTRY: 0x%x" % bv.entry_point -print "ARCH: %s" % bv.arch.name -print "\n-------- Function List --------" - -for func in bv.functions: - print func.symbol.name - - -print "\n-------- First 10 strings --------" - -for i in xrange(10): - start = bv.strings[i].start - length = bv.strings[i].length - string = bv.read(start,length) - print "0x%x (%d):\t%s" % (start, length, string) diff --git a/python/examples/bin_info.py b/python/examples/bin_info.py new file mode 100644 index 00000000..48073894 --- /dev/null +++ b/python/examples/bin_info.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +import sys, binaryninja, time +if sys.platform.lower().startswith("linux"): + bintype="ELF" +elif sys.platform.lower() == "darwin": + bintype="Mach-O" +else: + raise Exception, "%s is not supported on this plugin" % sys.platform + +if len(sys.argv) > 1: + target = sys.argv[1] +else: + target = "/bin/ls" + +bv = binaryninja.BinaryViewType[bintype].open(target) +bv.update_analysis_and_wait() + +print "-------- %s --------" % target +print "START: 0x%x" % bv.start +print "ENTRY: 0x%x" % bv.entry_point +print "ARCH: %s" % bv.arch.name +print "\n-------- Function List --------" + +for func in bv.functions: + print func.symbol.name + + +print "\n-------- First 10 strings --------" + +for i in xrange(10): + start = bv.strings[i].start + length = bv.strings[i].length + string = bv.read(start,length) + print "0x%x (%d):\t%s" % (start, length, string) diff --git a/python/examples/export-svg.py b/python/examples/export-svg.py deleted file mode 100755 index 02a6d57e..00000000 --- a/python/examples/export-svg.py +++ /dev/null @@ -1,166 +0,0 @@ -from binaryninja import * -import os,sys - -escape_table = { - "'": "'", - ">": ">", - "<": "<", - '"': """, - ' ': " " -} - -def escape(string): - string=string.decode('utf-8').encode('ascii','xmlcharrefreplace') #handle extended unicode - return ''.join(escape_table.get(i,i) for i in string) #still escape the basics - -def save_svg(bv,function): - filename = os.path.basename(bv.file.filename) - address = hex(function.start).replace('L','') - outputfile = os.path.join(os.path.expanduser('~'), 'binaryninja-{filename}-{function}.html'.format(filename=filename,function=address)) - content = render_svg(function) - output = open(outputfile,'w') - output.write(content) - output.close() - #os.system('open %s' % outputfile) - -def instruction_data_flow(function,address): - ''' TODO: Extract data flow information ''' - length = function.view.get_instruction_length(function.arch,address) - bytes = function.view.read(address, length) - hex = bytes.encode('hex') - padded = ' '.join([hex[i:i+2] for i in range(0, len(hex), 2)]) - return 'Opcode: {bytes}'.format(bytes=padded) - -def render_svg(function): - graph = function.create_graph() - graph.layout_and_wait() - heightconst = 15 - ratio = 0.48 - widthconst = heightconst*ratio - - output = ''' - - - - -''' - output += ''' - - - - - - - - - - - - - - - '''.format(width=graph.width*widthconst, height=graph.height*heightconst) - output += ''' - Function Graph 0 - ''' - edges = '' - for i,block in enumerate(graph.blocks): - - #Calculate basic block location and coordinates - x = ((block.x) * widthconst) - y = ((block.y) * heightconst) - width = ((block.width) * widthconst) - height = ((block.height) * heightconst) - - #Render block - output += ' \n'.format(i=i) - output += ' Basic Block {i}\n'.format(i=i) - output += ' \n'.format(x=x,y=y,width=width,height=height) - - #Render instructions, unfortunately tspans don't allow copying/pasting more - #than one line at a time, need SVG 1.2 textarea tags for that it looks like - - output += ' \n'.format(x=x,y=y + (i + 1) * heightconst) - for i,line in enumerate(block.lines): - output += ' '.format(x=x,y=y + (i + 0.7) * heightconst,address=hex(line.address)[:-1]) - hover = instruction_data_flow(function, line.address) - output += '{hover}'.format(hover=hover) - for token in line.tokens: - # TODO: add hover for hex, function, and reg tokens - output+='{text}'.format(text=escape(token.text),tokentype=token.type) - output += '\n' - output += ' \n' - output += ' \n' - - #Edges are rendered in a seperate chunk so they have priority over the - #basic blocks or else they'd render below them - - for edge in block.outgoing_edges: - points = "" - for x,y in edge.points: - points += str(x*widthconst)+","+str(y*heightconst) + " " - edges += ' \n'.format(type=edge.type,points=points) - output += ' ' + edges + '\n' - output += ' \n' - output += '' - return output - -PluginCommand.register_for_function("Export to SVG", "Exports an SVG of the current function to your home folder.", save_svg) diff --git a/python/examples/export_svg.py b/python/examples/export_svg.py new file mode 100755 index 00000000..02a6d57e --- /dev/null +++ b/python/examples/export_svg.py @@ -0,0 +1,166 @@ +from binaryninja import * +import os,sys + +escape_table = { + "'": "'", + ">": ">", + "<": "<", + '"': """, + ' ': " " +} + +def escape(string): + string=string.decode('utf-8').encode('ascii','xmlcharrefreplace') #handle extended unicode + return ''.join(escape_table.get(i,i) for i in string) #still escape the basics + +def save_svg(bv,function): + filename = os.path.basename(bv.file.filename) + address = hex(function.start).replace('L','') + outputfile = os.path.join(os.path.expanduser('~'), 'binaryninja-{filename}-{function}.html'.format(filename=filename,function=address)) + content = render_svg(function) + output = open(outputfile,'w') + output.write(content) + output.close() + #os.system('open %s' % outputfile) + +def instruction_data_flow(function,address): + ''' TODO: Extract data flow information ''' + length = function.view.get_instruction_length(function.arch,address) + bytes = function.view.read(address, length) + hex = bytes.encode('hex') + padded = ' '.join([hex[i:i+2] for i in range(0, len(hex), 2)]) + return 'Opcode: {bytes}'.format(bytes=padded) + +def render_svg(function): + graph = function.create_graph() + graph.layout_and_wait() + heightconst = 15 + ratio = 0.48 + widthconst = heightconst*ratio + + output = ''' + + + + +''' + output += ''' + + + + + + + + + + + + + + + '''.format(width=graph.width*widthconst, height=graph.height*heightconst) + output += ''' + Function Graph 0 + ''' + edges = '' + for i,block in enumerate(graph.blocks): + + #Calculate basic block location and coordinates + x = ((block.x) * widthconst) + y = ((block.y) * heightconst) + width = ((block.width) * widthconst) + height = ((block.height) * heightconst) + + #Render block + output += ' \n'.format(i=i) + output += ' Basic Block {i}\n'.format(i=i) + output += ' \n'.format(x=x,y=y,width=width,height=height) + + #Render instructions, unfortunately tspans don't allow copying/pasting more + #than one line at a time, need SVG 1.2 textarea tags for that it looks like + + output += ' \n'.format(x=x,y=y + (i + 1) * heightconst) + for i,line in enumerate(block.lines): + output += ' '.format(x=x,y=y + (i + 0.7) * heightconst,address=hex(line.address)[:-1]) + hover = instruction_data_flow(function, line.address) + output += '{hover}'.format(hover=hover) + for token in line.tokens: + # TODO: add hover for hex, function, and reg tokens + output+='{text}'.format(text=escape(token.text),tokentype=token.type) + output += '\n' + output += ' \n' + output += ' \n' + + #Edges are rendered in a seperate chunk so they have priority over the + #basic blocks or else they'd render below them + + for edge in block.outgoing_edges: + points = "" + for x,y in edge.points: + points += str(x*widthconst)+","+str(y*heightconst) + " " + edges += ' \n'.format(type=edge.type,points=points) + output += ' ' + edges + '\n' + output += ' \n' + output += '' + return output + +PluginCommand.register_for_function("Export to SVG", "Exports an SVG of the current function to your home folder.", save_svg) diff --git a/python/examples/instruction-iterator.py b/python/examples/instruction-iterator.py deleted file mode 100644 index 43bc000e..00000000 --- a/python/examples/instruction-iterator.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python - -import sys -try: - import binaryninja -except ImportError: - sys.path.append("/Applications/Binary Ninja.app/Contents/Resources/python/") - import binaryninja -import time - -if sys.platform.lower().startswith("linux"): - bintype="ELF" -elif sys.platform.lower() == "darwin": - bintype="Mach-O" -else: - raise Exception, "%s is not supported on this plugin" % sys.platform - -if len(sys.argv) > 1: - target = sys.argv[1] -else: - target = "/bin/ls" - -bv = binaryninja.BinaryViewType[bintype].open(target) -bv.update_analysis_and_wait() - -print "-------- %s --------" % target -print "START: 0x%x" % bv.start -print "ENTRY: 0x%x" % bv.entry_point -print "ARCH: %s" % bv.arch.name -print "\n-------- Function List --------" - -""" print all the functions, their basic blocks, and their il instructions """ -for func in bv.functions: - print repr(func) - for block in func.low_level_il: - print "\t{0}".format(block) - - for insn in block: - print "\t\t{0}".format(insn) - - -""" print all the functions, their basic blocks, and their mc instructions """ -for func in bv.functions: - print repr(func) - for block in func: - print "\t{0}".format(block) - - for insn in block: - print "\t\t{0}".format(insn) diff --git a/python/examples/instruction_iterator.py b/python/examples/instruction_iterator.py new file mode 100644 index 00000000..43bc000e --- /dev/null +++ b/python/examples/instruction_iterator.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import sys +try: + import binaryninja +except ImportError: + sys.path.append("/Applications/Binary Ninja.app/Contents/Resources/python/") + import binaryninja +import time + +if sys.platform.lower().startswith("linux"): + bintype="ELF" +elif sys.platform.lower() == "darwin": + bintype="Mach-O" +else: + raise Exception, "%s is not supported on this plugin" % sys.platform + +if len(sys.argv) > 1: + target = sys.argv[1] +else: + target = "/bin/ls" + +bv = binaryninja.BinaryViewType[bintype].open(target) +bv.update_analysis_and_wait() + +print "-------- %s --------" % target +print "START: 0x%x" % bv.start +print "ENTRY: 0x%x" % bv.entry_point +print "ARCH: %s" % bv.arch.name +print "\n-------- Function List --------" + +""" print all the functions, their basic blocks, and their il instructions """ +for func in bv.functions: + print repr(func) + for block in func.low_level_il: + print "\t{0}".format(block) + + for insn in block: + print "\t\t{0}".format(insn) + + +""" print all the functions, their basic blocks, and their mc instructions """ +for func in bv.functions: + print repr(func) + for block in func: + print "\t{0}".format(block) + + for insn in block: + print "\t\t{0}".format(insn) diff --git a/python/examples/jump-table.py b/python/examples/jump-table.py deleted file mode 100644 index 39fed1a5..00000000 --- a/python/examples/jump-table.py +++ /dev/null @@ -1,63 +0,0 @@ -# This plugin will attempt to resolve simple jump tables (an array of code pointers) and add the destinations -# as indirect branch targets so that the flow graph reflects the jump table's control flow. -from binaryninja import * -import struct - -def find_jump_table(bv, addr): - for block in bv.get_basic_blocks_at(addr): - func = block.function - arch = func.arch - addrsize = arch.address_size - - # Grab the instruction tokens so that we can look for the table's starting address - tokens, length = arch.get_instruction_text(bv.read(addr, 16), addr) - - # Look for the next jump instruction, which may be the current instruction. Some jump tables will - # compute the address first then jump to the computed address as a separate instruction. - jump_addr = addr - while jump_addr < block.end: - info = arch.get_instruction_info(bv.read(jump_addr, 16), jump_addr) - if len(info.branches) != 0: - break - jump_addr += info.length - if jump_addr >= block.end: - print "Unable to find jump after instruction 0x%x" % addr - continue - print "Jump at 0x%x" % jump_addr - - # Collect the branch targets for any tables referenced by the clicked instruction - branches = [] - for token in tokens: - if token.type == "PossibleAddressToken": # Table addresses will be a "possible address" token - tbl = token.value - print "Found possible table at 0x%x" % tbl - i = 0 - while True: - # Read the next pointer from the table - data = bv.read(tbl + (i * addrsize), addrsize) - if len(data) == addrsize: - if addrsize == 4: - ptr = struct.unpack("= bv.start) and (ptr < bv.end): - print "Found destination 0x%x" % ptr - branches.append((arch, ptr)) - else: - # Once a value that is not a pointer is encountered, the jump table is ended - break - else: - # Reading invalid memory - break - - i += 1 - - # Set the indirect branch targets on the jump instruction to be the list of targets discovered - func.set_user_indirect_branches(arch, jump_addr, branches) - -# Create a plugin command so that the user can right click on an instruction referencing a jump table and -# invoke the command -PluginCommand.register_for_address("Process jump table", "Look for jump table destinations", find_jump_table) diff --git a/python/examples/jump_table.py b/python/examples/jump_table.py new file mode 100644 index 00000000..39fed1a5 --- /dev/null +++ b/python/examples/jump_table.py @@ -0,0 +1,63 @@ +# This plugin will attempt to resolve simple jump tables (an array of code pointers) and add the destinations +# as indirect branch targets so that the flow graph reflects the jump table's control flow. +from binaryninja import * +import struct + +def find_jump_table(bv, addr): + for block in bv.get_basic_blocks_at(addr): + func = block.function + arch = func.arch + addrsize = arch.address_size + + # Grab the instruction tokens so that we can look for the table's starting address + tokens, length = arch.get_instruction_text(bv.read(addr, 16), addr) + + # Look for the next jump instruction, which may be the current instruction. Some jump tables will + # compute the address first then jump to the computed address as a separate instruction. + jump_addr = addr + while jump_addr < block.end: + info = arch.get_instruction_info(bv.read(jump_addr, 16), jump_addr) + if len(info.branches) != 0: + break + jump_addr += info.length + if jump_addr >= block.end: + print "Unable to find jump after instruction 0x%x" % addr + continue + print "Jump at 0x%x" % jump_addr + + # Collect the branch targets for any tables referenced by the clicked instruction + branches = [] + for token in tokens: + if token.type == "PossibleAddressToken": # Table addresses will be a "possible address" token + tbl = token.value + print "Found possible table at 0x%x" % tbl + i = 0 + while True: + # Read the next pointer from the table + data = bv.read(tbl + (i * addrsize), addrsize) + if len(data) == addrsize: + if addrsize == 4: + ptr = struct.unpack("= bv.start) and (ptr < bv.end): + print "Found destination 0x%x" % ptr + branches.append((arch, ptr)) + else: + # Once a value that is not a pointer is encountered, the jump table is ended + break + else: + # Reading invalid memory + break + + i += 1 + + # Set the indirect branch targets on the jump instruction to be the list of targets discovered + func.set_user_indirect_branches(arch, jump_addr, branches) + +# Create a plugin command so that the user can right click on an instruction referencing a jump table and +# invoke the command +PluginCommand.register_for_address("Process jump table", "Look for jump table destinations", find_jump_table) diff --git a/python/examples/version-switcher.py b/python/examples/version-switcher.py deleted file mode 100644 index 6199c578..00000000 --- a/python/examples/version-switcher.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -import sys -import binaryninja -import datetime - -chandefault = binaryninja.UpdateChannel.list[0].name -channel = None -versions = [] - -def load_channel(newchannel): - global channel - global versions - if (channel != None and newchannel == channel.name): - print "Same channel, not updating." - else: - try: - print "Loading channel %s" % newchannel - channel = binaryninja.UpdateChannel[newchannel] - print "Loading versions..." - versions = channel.versions - except Exception: - print "%s is not a valid channel name. Defaulting to " % chandefault - channel = binaryninja.UpdateChannel[chandefault] - -def select(version): - done = False - date = datetime.datetime.fromtimestamp(version.time).strftime('%c') - while not done: - print "Version:\t%s" % version.version - print "Updated:\t%s" % date - print "Notes:\n\n-----\n%s" % version.notes - print "-----" - print "\t1)\tSwitch to version" - print "\t2)\tMain Menu" - selection = raw_input('Choice: ') - if selection.isdigit(): - selection = int(selection) - else: - selection = 0 - if (selection == 2): - done = True - elif (selection == 1): - if (version.version == channel.latest_version.version): - print "Requesting update to latest version." - else: - print "Requesting update to prior version." - if binaryninja.are_auto_updates_enabled(): - print "Disabling automatic updates." - binaryninja.set_auto_updates_enabled(False) - if (version.version == binaryninja.core_version): - print "Already running %s" % version.version - else: - print "version.version %s" % version.version - print "binaryninja.core_version %s" % binaryninja.core_version - print "Updating..." - print version.update() - #forward updating won't work without reloading - sys.exit() - else: - print "Invalid selection" - -def list_channels(): - done = False - print "\tSelect channel:\n" - while not done: - channel_list = binaryninja.UpdateChannel.list - for index, item in enumerate(channel_list): - print "\t%d)\t%s" % (index+1, item.name) - print "\t%d)\t%s" % (len(channel_list)+1, "Main Menu") - selection = raw_input('Choice: ') - if selection.isdigit(): - selection = int(selection) - else: - selection = 0 - if (selection <= 0 or selection > len(channel_list)+1): - print "%s is an invalid choice." % selection - else: - done = True - if (selection != len(channel_list) + 1): - load_channel(channel_list[selection - 1].name) - -def toggle_updates(): - binaryninja.set_auto_updates_enabled(not binaryninja.are_auto_updates_enabled()) - -def main(): - global channel - done = False - load_channel(chandefault) - while not done: - print "\n\tBinary Ninja Version Switcher" - print "\t\tCurrent Channel:\t%s" % channel.name - print "\t\tCurrent Version:\t%s" % binaryninja.core_version - print "\t\tAuto-Updates On:\t%s\n" % binaryninja.are_auto_updates_enabled() - for index, version in enumerate(versions): - date = datetime.datetime.fromtimestamp(version.time).strftime('%c') - print "\t%d)\t%s (%s)" % (index + 1, version.version, date) - print "\t%d)\t%s" % (len(versions) + 1, "Switch Channel") - print "\t%d)\t%s" % (len(versions) + 2, "Toggle Auto Updates") - print "\t%d)\t%s" % (len(versions) + 3, "Exit") - selection = raw_input('Choice: ') - if selection.isdigit(): - selection = int(selection) - else: - selection = 0 - if (selection <= 0 or selection > len(versions) + 3): - print "%d is an invalid choice.\n\n" % selection - else: - if (selection == len(versions) + 3): - done = True - elif (selection == len(versions) + 2): - toggle_updates() - elif (selection == len(versions) + 1): - list_channels() - else: - select(versions[selection - 1]) - - -if __name__ == "__main__": - main() diff --git a/python/examples/version_switcher.py b/python/examples/version_switcher.py new file mode 100644 index 00000000..6199c578 --- /dev/null +++ b/python/examples/version_switcher.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +import sys +import binaryninja +import datetime + +chandefault = binaryninja.UpdateChannel.list[0].name +channel = None +versions = [] + +def load_channel(newchannel): + global channel + global versions + if (channel != None and newchannel == channel.name): + print "Same channel, not updating." + else: + try: + print "Loading channel %s" % newchannel + channel = binaryninja.UpdateChannel[newchannel] + print "Loading versions..." + versions = channel.versions + except Exception: + print "%s is not a valid channel name. Defaulting to " % chandefault + channel = binaryninja.UpdateChannel[chandefault] + +def select(version): + done = False + date = datetime.datetime.fromtimestamp(version.time).strftime('%c') + while not done: + print "Version:\t%s" % version.version + print "Updated:\t%s" % date + print "Notes:\n\n-----\n%s" % version.notes + print "-----" + print "\t1)\tSwitch to version" + print "\t2)\tMain Menu" + selection = raw_input('Choice: ') + if selection.isdigit(): + selection = int(selection) + else: + selection = 0 + if (selection == 2): + done = True + elif (selection == 1): + if (version.version == channel.latest_version.version): + print "Requesting update to latest version." + else: + print "Requesting update to prior version." + if binaryninja.are_auto_updates_enabled(): + print "Disabling automatic updates." + binaryninja.set_auto_updates_enabled(False) + if (version.version == binaryninja.core_version): + print "Already running %s" % version.version + else: + print "version.version %s" % version.version + print "binaryninja.core_version %s" % binaryninja.core_version + print "Updating..." + print version.update() + #forward updating won't work without reloading + sys.exit() + else: + print "Invalid selection" + +def list_channels(): + done = False + print "\tSelect channel:\n" + while not done: + channel_list = binaryninja.UpdateChannel.list + for index, item in enumerate(channel_list): + print "\t%d)\t%s" % (index+1, item.name) + print "\t%d)\t%s" % (len(channel_list)+1, "Main Menu") + selection = raw_input('Choice: ') + if selection.isdigit(): + selection = int(selection) + else: + selection = 0 + if (selection <= 0 or selection > len(channel_list)+1): + print "%s is an invalid choice." % selection + else: + done = True + if (selection != len(channel_list) + 1): + load_channel(channel_list[selection - 1].name) + +def toggle_updates(): + binaryninja.set_auto_updates_enabled(not binaryninja.are_auto_updates_enabled()) + +def main(): + global channel + done = False + load_channel(chandefault) + while not done: + print "\n\tBinary Ninja Version Switcher" + print "\t\tCurrent Channel:\t%s" % channel.name + print "\t\tCurrent Version:\t%s" % binaryninja.core_version + print "\t\tAuto-Updates On:\t%s\n" % binaryninja.are_auto_updates_enabled() + for index, version in enumerate(versions): + date = datetime.datetime.fromtimestamp(version.time).strftime('%c') + print "\t%d)\t%s (%s)" % (index + 1, version.version, date) + print "\t%d)\t%s" % (len(versions) + 1, "Switch Channel") + print "\t%d)\t%s" % (len(versions) + 2, "Toggle Auto Updates") + print "\t%d)\t%s" % (len(versions) + 3, "Exit") + selection = raw_input('Choice: ') + if selection.isdigit(): + selection = int(selection) + else: + selection = 0 + if (selection <= 0 or selection > len(versions) + 3): + print "%d is an invalid choice.\n\n" % selection + else: + if (selection == len(versions) + 3): + done = True + elif (selection == len(versions) + 2): + toggle_updates() + elif (selection == len(versions) + 1): + list_channels() + else: + select(versions[selection - 1]) + + +if __name__ == "__main__": + main() -- cgit v1.3.1