1#!/usr/bin/env python
2"""
3ezhexviewer.py
4
5A simple hexadecimal viewer based on easygui. It should work on any platform
6with Python 2.x or 3.x.
7
8Usage: ezhexviewer.py [file]
9
10Usage in a python application:
11
12    import ezhexviewer
13    ezhexviewer.hexview_file(filename)
14    ezhexviewer.hexview_data(data)
15
16
17ezhexviewer project website: http://www.decalage.info/python/ezhexviewer
18
19ezhexviewer is copyright (c) 2012-2019, Philippe Lagadec (http://www.decalage.info)
20All rights reserved.
21
22Redistribution and use in source and binary forms, with or without modification,
23are permitted provided that the following conditions are met:
24
25 * Redistributions of source code must retain the above copyright notice, this
26   list of conditions and the following disclaimer.
27 * Redistributions in binary form must reproduce the above copyright notice,
28   this list of conditions and the following disclaimer in the documentation
29   and/or other materials provided with the distribution.
30
31THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
32ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
33WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
34DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
35FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
36DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
37SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
39OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
40OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41"""
42
43#------------------------------------------------------------------------------
44# CHANGELOG:
45# 2012-09-17 v0.01 PL: - first version
46# 2012-10-04 v0.02 PL: - added license
47# 2016-09-06 v0.50 PL: - added main function for entry points in setup.py
48# 2016-10-26       PL: - fixed to run on Python 2+3
49# 2017-03-23 v0.51 PL: - fixed display of control characters (issue #151)
50# 2017-04-26       PL: - fixed absolute imports (issue #141)
51# 2018-09-15 v0.54 PL: - easygui is now a dependency
52
53__version__ = '0.54'
54
55#-----------------------------------------------------------------------------
56# TODO:
57# + options to set title and msg
58
59# === IMPORTS ================================================================
60
61import sys, os
62
63# IMPORTANT: it should be possible to run oletools directly as scripts
64# in any directory without installing them with pip or setup.py.
65# In that case, relative imports are NOT usable.
66# And to enable Python 2+3 compatibility, we need to use absolute imports,
67# so we add the oletools parent folder to sys.path (absolute+normalized path):
68_thismodule_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
69# print('_thismodule_dir = %r' % _thismodule_dir)
70_parent_dir = os.path.normpath(os.path.join(_thismodule_dir, '..'))
71# print('_parent_dir = %r' % _thirdparty_dir)
72if not _parent_dir in sys.path:
73    sys.path.insert(0, _parent_dir)
74
75import easygui
76
77# === PYTHON 2+3 SUPPORT ======================================================
78
79if sys.version_info[0] >= 3:
80    # Python 3 specific adaptations
81    # py3 range = py2 xrange
82    xrange = range
83    PYTHON3 = True
84else:
85    PYTHON3 = False
86
87def xord(char):
88    '''
89    workaround for ord() to work on characters from a bytes string with
90    Python 2 and 3. If s is a bytes string, s[i] is a bytes string of
91    length 1 on Python 2, but it is an integer on Python 3...
92    xord(c) returns ord(c) if c is a bytes string, or c if it is already
93    an integer.
94    :param char: int or bytes of length 1
95    :return: ord(c) if bytes, c if int
96    '''
97    if isinstance(char, int):
98        return char
99    else:
100        return ord(char)
101
102def bchr(x):
103    '''
104    workaround for chr() to return a bytes string of length 1 with
105    Python 2 and 3. On Python 3, chr returns a unicode string, but
106    on Python 2 it is a bytes string.
107    bchr() always returns a bytes string on Python 2+3.
108    :param x: int
109    :return: chr(x) as a bytes string
110    '''
111    if PYTHON3:
112        # According to the Python 3 documentation, bytes() can be
113        # initialized with an iterable:
114        return bytes([x])
115    else:
116        return chr(x)
117
118#------------------------------------------------------------------------------
119# The following code (hexdump3 only) is a modified version of the hex dumper
120# recipe published on ASPN by Sebastien Keim and Raymond Hattinger under the
121# PSF license. I added the startindex parameter.
122# see http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/142812
123# PSF license: http://docs.python.org/license.html
124# Copyright (c) 2001-2012 Python Software Foundation; All Rights Reserved
125
126FILTER = b''.join([(len(repr(bchr(x)))<=4 and x>=0x20) and bchr(x) or b'.' for x in range(256)])
127
128def hexdump3(src, length=8, startindex=0):
129    """
130    Returns a hexadecimal dump of a binary string.
131    length: number of bytes per row.
132    startindex: index of 1st byte.
133    """
134    result=[]
135    for i in xrange(0, len(src), length):
136        s = src[i:i+length]
137        hexa = ' '.join(["%02X" % xord(x) for x in s])
138        printable = s.translate(FILTER)
139        if PYTHON3:
140            # On Python 3, need to convert printable from bytes to str:
141            printable = printable.decode('latin1')
142        result.append("%08X   %-*s   %s\n" % (i+startindex, length*3, hexa, printable))
143    return ''.join(result)
144
145# end of PSF-licensed code.
146#------------------------------------------------------------------------------
147
148
149def hexview_data (data, msg='', title='ezhexviewer', length=16, startindex=0):
150    hex = hexdump3(data, length=length, startindex=startindex)
151    easygui.codebox(msg=msg, title=title, text=hex)
152
153
154def hexview_file (filename, msg='', title='ezhexviewer', length=16, startindex=0):
155    data = open(filename, 'rb').read()
156    hexview_data(data, msg=msg, title=title, length=length, startindex=startindex)
157
158
159# === MAIN ===================================================================
160
161def main():
162    try:
163        filename = sys.argv[1]
164    except:
165        filename = easygui.fileopenbox()
166    if filename:
167        try:
168            hexview_file(filename, msg='File: %s' % filename)
169        except:
170            easygui.exceptionbox(msg='Error:', title='ezhexviewer')
171
172
173if __name__ == '__main__':
174    main()
175