Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
Ben Jackson
4d4f85233d Add offset support, sort of 2021-06-09 11:25:38 +01:00
Ben Jackson
58a1c05465 Ask user for how many bytes to read 2021-06-09 11:25:38 +01:00
Ben Jackson
ae6572dde6 Make sure the buffer is not marked modified 2021-06-09 11:25:38 +01:00
Ben Jackson
9745d55919 Add a little header 2021-06-09 11:25:38 +01:00
Ben Jackson
f09cd89384 Make a sort of memory view work by dumping using hexdump.py 2021-06-09 11:25:38 +01:00
Ben Jackson
61a62c5ab5 Update CodeLLDB to support readMemeory 2021-06-09 11:25:38 +01:00
Ben Jackson
ba9cb2f6d3 WIP: First sort-of untested attempt at readmemory request 2021-06-09 11:25:38 +01:00
8 changed files with 596 additions and 8 deletions

View file

@ -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

View file

@ -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

View file

@ -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
},
} )

View file

@ -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': {

View file

@ -54,7 +54,8 @@ DEFAULTS = {
'variables': {
'expand_collapse': [ '<CR>', '<2-LeftMouse>' ],
'delete': [ '<Del>' ],
'set_value': [ '<C-CR>', '<leader><CR>' ]
'set_value': [ '<C-CR>', '<leader><CR>' ],
'read_memory': [ '<leader>m' ],
},
'stack_trace': {
'expand_or_jump': [ '<CR>', '<2-LeftMouse>' ],

View file

@ -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' ) )

View file

@ -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 <silent> <buffer> { mapping } '
':<C-u>call vimspector#SetVariableValue()<CR>' )
for mapping in utils.GetVimList( mappings, 'read_memory' ):
vim.command( f'nnoremap <silent> <buffer> { mapping } '
':<C-u>call vimspector#ReadMemory()<CR>' )
class VariablesView( object ):
@ -187,6 +205,8 @@ class VariablesView( object ):
if utils.UseWinBar():
vim.command( 'nnoremenu <silent> 1.1 WinBar.Set '
':call vimspector#SetVariableValue()<CR>' )
vim.command( 'nnoremenu <silent> 1.2 WinBar.Memory '
':call vimspector#ReadMemory()<CR>' )
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()<CR>' )
vim.command( 'nnoremenu <silent> 1.3 WinBar.Delete '
':call vimspector#DeleteWatch()<CR>' )
vim.command( 'nnoremenu <silent> 1.1 WinBar.Set '
vim.command( 'nnoremenu <silent> 1.4 WinBar.Set '
':call vimspector#SetVariableValue()<CR>' )
vim.command( 'nnoremenu <silent> 1.5 WinBar.Memory '
':call vimspector#ReadMemory()<CR>' )
# 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', '<unknown>' )
)
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', '' ),

466
python3/vimspector/vendor/hexdump.py vendored Executable file
View file

@ -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 <techtonik@gmail.com>'
__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