diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index 78c7c1b..0eb394f 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -223,6 +223,13 @@ function! vimspector#SetVariableValue( ... ) abort endif endfunction +function! vimspector#ReadMemory() abort + if !s:Enabled() + return + endif + py3 _vimspector_session.ReadMemory() +endfunction + function! vimspector#DeleteWatch() abort if !s:Enabled() return diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index 98aeca5..5f54e1c 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -16,6 +16,7 @@ import vim import logging import json +import os from collections import defaultdict from vimspector import utils, terminal, signs @@ -40,6 +41,7 @@ class CodeView( object ): 'breakpoints': [] } self._current_frame = None + self._scratch_buffers = [] with utils.LetCurrentWindow( self._window ): if utils.UseWinBar(): @@ -173,6 +175,10 @@ class CodeView( object ): self.ClearBreakpoints() self.Clear() + for b in self._scratch_buffers: + utils.CleanUpHiddenBuffer( b ) + self._scratch_buffers = [] + def AddBreakpoints( self, source, breakpoints ): for breakpoint in breakpoints: source = breakpoint.get( 'source' ) or source @@ -287,3 +293,32 @@ class CodeView( object ): # FIXME: Change this tor return the PID rather than having debug_session # work that out return self._terminal.buffer_number + + + def ShowMemory( self, memoryReference, length, offset, msg ): + if not self._window.valid: + return False + + buf_name = os.path.join( '_vimspector_mem', memoryReference ) + buf = utils.BufferForFile( buf_name ) + self._scratch_buffers.append( buf ) + utils.SetUpHiddenBuffer( buf, buf_name ) + with utils.ModifiableScratchBuffer( buf ): + # TODO: The data is encoded in base64, so we need to convert that to the + # equivalent output of say xxd + data = msg.get( 'body', {} ).get( 'data', '' ) + utils.SetBufferContents( buf, [ + f'Memory Dump for Reference {memoryReference} Length: {length} bytes' + f' Offset: {offset}', + '-' * 80, + 'Offset Bytes Text', + '-' * 80, + ] ) + utils.AppendToBuffer( buf, utils.Base64ToHexDump( data ) ) + + utils.SetSyntax( '', 'xxd', buf ) + utils.JumpToWindow( self._window ) + utils.OpenFileInCurrentWindow( buf_name ) + + # TODO: Need to set up some mappings here that allow the user to browse + # around by setting the offset diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 36ad62b..c8c6ce8 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -535,6 +535,45 @@ class DebugSession( object ): def SetVariableValue( self, new_value = None, buf = None, line_num = None ): self._variablesView.SetVariableValue( new_value, buf, line_num ) + @IfConnected() + def ReadMemory( self, offset = None, buf = None, line_num = None ): + if not self._server_capabilities.get( 'supportsReadMemoryRequest' ): + utils.UserMessage( "Server does not support memory request", + error = True ) + return + + memoryReference = self._variablesView.GetMemoryReference( buf, line_num ) + if memoryReference is None: + utils.UserMessage( "Cannot find memory reference for that", + error = True ) + return + + length = utils.AskForInput( 'How much data to display? ', + default_value = '1024' ) + + if length is None: + return + + offset = utils.AskForInput( 'Location offset? ', + default_value = '0' ) + + if offset is None: + return + + + def handler( msg ): + self._codeView.ShowMemory( memoryReference, length, offset, msg ) + + self._connection.DoRequest( handler, { + 'command': 'readMemory', + 'arguments': { + 'memoryReference': memoryReference, + 'count': int( length ), + 'offset': int( offset ) + } + } ) + + @IfConnected() def AddWatch( self, expression ): self._variablesView.AddWatch( self._stackTraceView.GetCurrentFrame(), @@ -1163,7 +1202,8 @@ class DebugSession( object ): 'pathFormat': 'path', 'supportsVariableType': True, 'supportsVariablePaging': False, - 'supportsRunInTerminalRequest': True + 'supportsRunInTerminalRequest': True, + 'supportsMemoryReferences': True }, } ) diff --git a/python3/vimspector/gadgets.py b/python3/vimspector/gadgets.py index 23a298c..140c9f4 100644 --- a/python3/vimspector/gadgets.py +++ b/python3/vimspector/gadgets.py @@ -394,12 +394,12 @@ GADGETS = { '${version}/${file_name}', }, 'all': { - 'version': 'v1.6.1', + 'version': 'v1.6.4', }, 'macos': { 'file_name': 'codelldb-x86_64-darwin.vsix', 'checksum': - 'b1c998e7421beea9f3ba21aa5706210bb2249eba93c99b809247ee831075262f', + 'aa920b7b7d2ad4e9d70086355841b0b4844fb5f62cdea1296904100a1b660776', 'make_executable': [ 'adapter/codelldb', 'lldb/bin/debugserver', @@ -410,7 +410,7 @@ GADGETS = { 'linux': { 'file_name': 'codelldb-x86_64-linux.vsix', 'checksum': - 'f2a36cb6971fd95a467cf1a7620e160914e8f11bf82929932ee0aa5afbf6ae6a', + '', 'make_executable': [ 'adapter/codelldb', 'lldb/bin/lldb', @@ -421,7 +421,7 @@ GADGETS = { 'windows': { 'file_name': 'codelldb-x86_64-windows.vsix', 'checksum': - 'ca6a6525bf7719dc95265dc630b3cc817a8c0393b756fd242b710805ffdfb940', + '', 'make_executable': [] }, 'adapters': { diff --git a/python3/vimspector/settings.py b/python3/vimspector/settings.py index 89378af..60e8341 100644 --- a/python3/vimspector/settings.py +++ b/python3/vimspector/settings.py @@ -54,7 +54,8 @@ DEFAULTS = { 'variables': { 'expand_collapse': [ '', '<2-LeftMouse>' ], 'delete': [ '' ], - 'set_value': [ '', '' ] + 'set_value': [ '', '' ], + 'read_memory': [ 'm' ], }, 'stack_trace': { 'expand_or_jump': [ '', '<2-LeftMouse>' ], diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 5f836fc..150ab95 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -25,7 +25,9 @@ import shlex import collections import re import typing +import base64 +from vimspector.vendor.hexdump import hexdump LOG_FILE = os.path.expanduser( os.path.join( '~', '.vimspector.log' ) ) @@ -864,3 +866,8 @@ def UseWinBar(): # Buggy neovim doesn't render correctly when the WinBar is defined: # https://github.com/neovim/neovim/issues/12689 return not int( Call( 'has', 'nvim' ) ) + + +def Base64ToHexDump( data ): + data = base64.b64decode( data ) + return list( hexdump( data, 'generator' ) ) diff --git a/python3/vimspector/variables.py b/python3/vimspector/variables.py index 8dcb493..8524d67 100644 --- a/python3/vimspector/variables.py +++ b/python3/vimspector/variables.py @@ -56,6 +56,11 @@ class Expandable: def VariablesReference( self ): assert False + @abc.abstractmethod + def MemoryReference( self ): + assert None + + class Scope( Expandable ): """Holds an expandable scope (a DAP scope dict), with expand/collapse state""" @@ -66,6 +71,9 @@ class Scope( Expandable ): def VariablesReference( self ): return self.scope.get( 'variablesReference', 0 ) + def MemoryReference( self ): + return None + def Update( self, scope ): self.scope = scope @@ -81,6 +89,9 @@ class WatchResult( Expandable ): def VariablesReference( self ): return self.result.get( 'variablesReference', 0 ) + def MemoryReference( self ): + return self.result.get( 'memoryReference' ) + def Update( self, result ): self.changed = False if self.result[ 'result' ] != result[ 'result' ]: @@ -105,6 +116,9 @@ class Variable( Expandable ): def VariablesReference( self ): return self.variable.get( 'variablesReference', 0 ) + def MemoryReference( self ): + return self.variable.get( 'memoryReference' ) + def Update( self, variable ): self.changed = False if self.variable[ 'value' ] != variable[ 'value' ]: @@ -163,6 +177,10 @@ def AddExpandMappings( mappings = None ): for mapping in utils.GetVimList( mappings, 'set_value' ): vim.command( f'nnoremap { mapping } ' ':call vimspector#SetVariableValue()' ) + for mapping in utils.GetVimList( mappings, 'read_memory' ): + vim.command( f'nnoremap { mapping } ' + ':call vimspector#ReadMemory()' ) + class VariablesView( object ): @@ -187,6 +205,8 @@ class VariablesView( object ): if utils.UseWinBar(): vim.command( 'nnoremenu 1.1 WinBar.Set ' ':call vimspector#SetVariableValue()' ) + vim.command( 'nnoremenu 1.2 WinBar.Memory ' + ':call vimspector#ReadMemory()' ) AddExpandMappings( mappings ) # Set up the "Watches" buffer in the watches_win (and create a WinBar in @@ -211,8 +231,10 @@ class VariablesView( object ): ':call vimspector#ExpandVariable()' ) vim.command( 'nnoremenu 1.3 WinBar.Delete ' ':call vimspector#DeleteWatch()' ) - vim.command( 'nnoremenu 1.1 WinBar.Set ' + vim.command( 'nnoremenu 1.4 WinBar.Set ' ':call vimspector#SetVariableValue()' ) + vim.command( 'nnoremenu 1.5 WinBar.Memory ' + ':call vimspector#ReadMemory()' ) # Set the (global!) balloon expr if supported has_balloon = int( vim.eval( "has( 'balloon_eval' )" ) ) @@ -580,6 +602,14 @@ class VariablesView( object ): }, failure_handler = failure_handler ) + def GetMemoryReference( self, buf = None, line_num = None ): + # Get a memoryReference for use in a ReadMemory request + variable, _ = self._GetVariable( buf, line_num ) + if variable is None: + return None + + return variable.MemoryReference() + def _DrawVariables( self, view, variables, indent, is_short = False ): assert indent > 0 @@ -595,10 +625,12 @@ class VariablesView( object ): value = variable.variable.get( 'value', '' ) ) else: + marker = 'm' if variable.MemoryReference() is not None else ' ' + marker += '*' if variable.changed else ' ' text = '{indent}{marker}{icon} {name} ({type_}): {value}'.format( # We borrow 1 space of indent to draw the change marker indent = ' ' * ( indent - 1 ), - marker = '*' if variable.changed else ' ', + marker = marker, icon = '+' if ( variable.IsExpandable() and not variable.IsExpanded() ) else '-', name = variable.variable.get( 'name', '' ), diff --git a/python3/vimspector/vendor/hexdump.py b/python3/vimspector/vendor/hexdump.py new file mode 100755 index 0000000..33b9b6b --- /dev/null +++ b/python3/vimspector/vendor/hexdump.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python +# -*- coding: latin-1 -*- + +# <-- removing this magic comment breaks Python 3.4 on Windows +""" +1. Dump binary data to the following text format: + +00000000: 00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... +00000010: 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........ + +It is similar to the one used by: +Scapy +00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... +00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........ + +Far Manager +000000000: 00 00 00 5B 68 65 78 64 ¦ 75 6D 70 5D 00 00 00 00 [hexdump] +000000010: 00 11 22 33 44 55 66 77 ¦ 88 99 AA BB CC DD EE FF ?"3DUfwˆ™ª»ÌÝîÿ + + +2. Restore binary data from the formats above as well + as from less exotic strings of raw hex + +""" + +__version__ = '3.3' +__author__ = 'anatoly techtonik ' +__license__ = 'Public Domain' + +__history__ = \ +""" +3.3 (2015-01-22) + * accept input from sys.stdin if "-" is specified + for both dump and restore (issue #1) + * new normalize_py() helper to set sys.stdout to + binary mode on Windows + +3.2 (2015-07-02) + * hexdump is now packaged as .zip on all platforms + (on Linux created archive was tar.gz) + * .zip is executable! try `python hexdump-3.2.zip` + * dump() now accepts configurable separator, patch + by Ian Land (PR #3) + +3.1 (2014-10-20) + * implemented workaround against mysterious coding + issue with Python 3 (see revision 51302cf) + * fix Python 3 installs for systems where UTF-8 is + not default (Windows), thanks to George Schizas + (the problem was caused by reading of README.txt) + +3.0 (2014-09-07) + * remove unused int2byte() helper + * add dehex(text) helper to convert hex string + to binary data + * add 'size' argument to dump() helper to specify + length of chunks + +2.0 (2014-02-02) + * add --restore option to command line mode to get + binary data back from hex dump + * support saving test output with `--test logfile` + * restore() from hex strings without spaces + * restore() now raises TypeError if input data is + not string + * hexdump() and dumpgen() now don't return unicode + strings in Python 2.x when generator is requested + +1.0 (2013-12-30) + * length of address is reduced from 10 to 8 + * hexdump() got new 'result' keyword argument, it + can be either 'print', 'generator' or 'return' + * actual dumping logic is now in new dumpgen() + generator function + * new dump(binary) function that takes binary data + and returns string like "66 6F 72 6D 61 74" + * new genchunks(mixed, size) function that chunks + both sequences and file like objects + +0.5 (2013-06-10) + * hexdump is now also a command line utility (no + restore yet) + +0.4 (2013-06-09) + * fix installation with Python 3 for non English + versions of Windows, thanks to George Schizas + +0.3 (2013-04-29) + * fully Python 3 compatible + +0.2 (2013-04-28) + * restore() to recover binary data from a hex dump in + native, Far Manager and Scapy text formats (others + might work as well) + * restore() is Python 3 compatible + +0.1 (2013-04-28) + * working hexdump() function for Python 2 +""" + +import binascii # binascii is required for Python 3 +import sys + +# --- constants +PY3K = sys.version_info >= (3, 0) + +# --- workaround against Python consistency issues +def normalize_py(): + ''' Problem 001 - sys.stdout in Python is by default opened in + text mode, and writes to this stdout produce corrupted binary + data on Windows + + python -c "import sys; sys.stdout.write('_\n_')" > file + python -c "print(repr(open('file', 'rb').read()))" + ''' + if sys.platform == "win32": + # set sys.stdout to binary mode on Windows + import os, msvcrt + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + +# --- - chunking helpers +def chunks(seq, size): + '''Generator that cuts sequence (bytes, memoryview, etc.) + into chunks of given size. If `seq` length is not multiply + of `size`, the lengh of the last chunk returned will be + less than requested. + + >>> list( chunks([1,2,3,4,5,6,7], 3) ) + [[1, 2, 3], [4, 5, 6], [7]] + ''' + d, m = divmod(len(seq), size) + for i in range(d): + yield seq[i*size:(i+1)*size] + if m: + yield seq[d*size:] + +def chunkread(f, size): + '''Generator that reads from file like object. May return less + data than requested on the last read.''' + c = f.read(size) + while len(c): + yield c + c = f.read(size) + +def genchunks(mixed, size): + '''Generator to chunk binary sequences or file like objects. + The size of the last chunk returned may be less than + requested.''' + if hasattr(mixed, 'read'): + return chunkread(mixed, size) + else: + return chunks(mixed, size) +# --- - /chunking helpers + + +def dehex(hextext): + """ + Convert from hex string to binary data stripping + whitespaces from `hextext` if necessary. + """ + if PY3K: + return bytes.fromhex(hextext) + else: + hextext = "".join(hextext.split()) + return hextext.decode('hex') + +def dump(binary, size=2, sep=' '): + ''' + Convert binary data (bytes in Python 3 and str in + Python 2) to hex string like '00 DE AD BE EF'. + `size` argument specifies length of text chunks + and `sep` sets chunk separator. + ''' + hexstr = binascii.hexlify(binary) + if PY3K: + hexstr = hexstr.decode('ascii') + return sep.join(chunks(hexstr.upper(), size)) + +def dumpgen(data): + ''' + Generator that produces strings: + + '00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................' + ''' + generator = genchunks(data, 16) + for addr, d in enumerate(generator): + # 00000000: + line = '%08X: ' % (addr*16) + # 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + dumpstr = dump(d) + line += dumpstr[:8*3] + if len(d) > 8: # insert separator if needed + line += ' ' + dumpstr[8*3:] + # ................ + # calculate indentation, which may be different for the last line + pad = 2 + if len(d) < 16: + pad += 3*(16 - len(d)) + if len(d) <= 8: + pad += 1 + line += ' '*pad + + for byte in d: + # printable ASCII range 0x20 to 0x7E + if not PY3K: + byte = ord(byte) + if 0x20 <= byte <= 0x7E: + line += chr(byte) + else: + line += '.' + yield line + +def hexdump(data, result='print'): + ''' + Transform binary data to the hex dump text format: + + 00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ + + [x] data argument as a binary string + [x] data argument as a file like object + + Returns result depending on the `result` argument: + 'print' - prints line by line + 'return' - returns single string + 'generator' - returns generator that produces lines + ''' + if PY3K and type(data) == str: + raise TypeError('Abstract unicode data (expected bytes sequence)') + + gen = dumpgen(data) + if result == 'generator': + return gen + elif result == 'return': + return '\n'.join(gen) + elif result == 'print': + for line in gen: + print(line) + else: + raise ValueError('Unknown value of `result` argument') + +def restore(dump): + ''' + Restore binary data from a hex dump. + [x] dump argument as a string + [ ] dump argument as a line iterator + + Supported formats: + [x] hexdump.hexdump + [x] Scapy + [x] Far Manager + ''' + minhexwidth = 2*16 # minimal width of the hex part - 00000... style + bytehexwidth = 3*16-1 # min width for a bytewise dump - 00 00 ... style + + result = bytes() if PY3K else '' + if type(dump) != str: + raise TypeError('Invalid data for restore') + + text = dump.strip() # ignore surrounding empty lines + for line in text.split('\n'): + # strip address part + addrend = line.find(':') + if 0 < addrend < minhexwidth: # : is not in ascii part + line = line[addrend+1:] + line = line.lstrip() + # check dump type + if line[2] == ' ': # 00 00 00 ... type of dump + # check separator + sepstart = (2+1)*7+2 # ('00'+' ')*7+'00' + sep = line[sepstart:sepstart+3] + if sep[:2] == ' ' and sep[2:] != ' ': # ...00 00 00 00... + hexdata = line[:bytehexwidth+1] + elif sep[2:] == ' ': # ...00 00 | 00 00... - Far Manager + hexdata = line[:sepstart] + line[sepstart+3:bytehexwidth+2] + else: # ...00 00 00 00... - Scapy, no separator + hexdata = line[:bytehexwidth] + line = hexdata + result += dehex(line) + return result + + +def runtest(logfile=None): + '''Run hexdump tests. Requires hexfile.bin to be in the same + directory as hexdump.py itself''' + + class TeeOutput(object): + def __init__(self, stream1, stream2): + self.outputs = [stream1, stream2] + + # -- methods from sys.stdout / sys.stderr + def write(self, data): + for stream in self.outputs: + if PY3K: + if 'b' in stream.mode: + data = data.encode('utf-8') + stream.write(data) + stream.flush() + + def tell(self): + raise IOError + + def flush(self): + for stream in self.outputs: + stream.flush() + # --/ sys.stdout + + if logfile: + openlog = open(logfile, 'wb') + # copy stdout and stderr streams to log file + savedstd = sys.stderr, sys.stdout + sys.stderr = TeeOutput(sys.stderr, openlog) + sys.stdout = TeeOutput(sys.stdout, openlog) + + + def echo(msg, linefeed=True): + sys.stdout.write(msg) + if linefeed: + sys.stdout.write('\n') + + expected = '''\ +00000000: 00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... +00000010: 00 11 22 33 44 55 66 77 88 99 0A BB CC DD EE FF .."3DUfw........\ +''' + + # get path to hexfile.bin + # this doesn't work from .zip + # import os.path as osp + # hexfile = osp.dirname(osp.abspath(__file__)) + '/hexfile.bin' + # this doesn't work either + # hexfile = osp.dirname(sys.modules[__name__].__file__) + '/hexfile.bin' + # this works + import pkgutil + bin = pkgutil.get_data('hexdump', 'data/hexfile.bin') + + # varios length of input data + hexdump(b'zzzz'*12) + hexdump(b'o'*17) + hexdump(b'p'*24) + hexdump(b'q'*26) + # allowable character set filter + hexdump(b'line\nfeed\r\ntest') + hexdump(b'\x00\x00\x00\x5B\x68\x65\x78\x64\x75\x6D\x70\x5D\x00\x00\x00\x00' + b'\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\x0A\xBB\xCC\xDD\xEE\xFF') + print('---') + # dumping file-like binary object to screen (default behavior) + hexdump(bin) + print('return output') + hexout = hexdump(bin, result='return') + assert hexout == expected, 'returned hex didn\'t match' + print('return generator') + hexgen = hexdump(bin, result='generator') + assert next(hexgen) == expected.split('\n')[0], 'hex generator 1 didn\'t match' + assert next(hexgen) == expected.split('\n')[1], 'hex generator 2 didn\'t match' + + # binary restore test + bindata = restore( +''' +00000000: 00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... +00000010: 00 11 22 33 44 55 66 77 88 99 0A BB CC DD EE FF .."3DUfw........ +''') + echo('restore check ', linefeed=False) + assert bin == bindata, 'restore check failed' + echo('passed') + + far = \ +''' +000000000: 00 00 00 5B 68 65 78 64 ¦ 75 6D 70 5D 00 00 00 00 [hexdump] +000000010: 00 11 22 33 44 55 66 77 ¦ 88 99 0A BB CC DD EE FF ?"3DUfwˆ™ª»ÌÝîÿ +''' + echo('restore far format ', linefeed=False) + assert bin == restore(far), 'far format check failed' + echo('passed') + + scapy = '''\ +00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... +00 11 22 33 44 55 66 77 88 99 0A BB CC DD EE FF .."3DUfw........ +''' + echo('restore scapy format ', linefeed=False) + assert bin == restore(scapy), 'scapy format check failed' + echo('passed') + + if not PY3K: + assert restore('5B68657864756D705D') == '[hexdump]', 'no space check failed' + assert dump('\\\xa1\xab\x1e', sep='').lower() == '5ca1ab1e' + else: + assert restore('5B68657864756D705D') == b'[hexdump]', 'no space check failed' + assert dump(b'\\\xa1\xab\x1e', sep='').lower() == '5ca1ab1e' + + print('---[test file hexdumping]---') + + import os + import tempfile + hexfile = tempfile.NamedTemporaryFile(delete=False) + try: + hexfile.write(bin) + hexfile.close() + hexdump(open(hexfile.name, 'rb')) + finally: + os.remove(hexfile.name) + if logfile: + sys.stderr, sys.stdout = savedstd + openlog.close() + + +def main(): + from optparse import OptionParser + parser = OptionParser(usage=''' + %prog [binfile|-] + %prog -r hexfile + %prog --test [logfile]''', version=__version__) + parser.add_option('-r', '--restore', action='store_true', + help='restore binary from hex dump') + parser.add_option('--test', action='store_true', help='run hexdump sanity checks') + + options, args = parser.parse_args() + + if options.test: + if args: + runtest(logfile=args[0]) + else: + runtest() + elif not args or len(args) > 1: + parser.print_help() + sys.exit(-1) + else: + ## dump file + if not options.restore: + # [x] memory effective dump + if args[0] == '-': + if not PY3K: + hexdump(sys.stdin) + else: + hexdump(sys.stdin.buffer) + else: + hexdump(open(args[0], 'rb')) + + ## restore file + else: + # prepare input stream + if args[0] == '-': + instream = sys.stdin + else: + if PY3K: + instream = open(args[0]) + else: + instream = open(args[0], 'rb') + + # output stream + # [ ] memory efficient restore + if PY3K: + sys.stdout.buffer.write(restore(instream.read())) + else: + # Windows - binary mode for sys.stdout to prevent data corruption + normalize_py() + sys.stdout.write(restore(instream.read())) + +if __name__ == '__main__': + main() + +# [x] file restore from command line utility +# [ ] write dump with LF on Windows for consistency +# [ ] encoding param for hexdump()ing Python 3 str if anybody requests that + +# [ ] document chunking API +# [ ] document hexdump API +# [ ] blog about sys.stdout text mode problem on Windows