Commit 86e216671b8e5d5fbc712734e294f62d950832c1

Authored by Philippe Lagadec
0 parents

Initial version of olebrowse and xxxswf2

oletools/LICENSE.txt 0 → 100644
  1 +++ a/oletools/LICENSE.txt
  1 +LICENSE for the oletools package:
  2 +
  3 +
  4 +Copyright (c) 2012, Philippe Lagadec (http://www.decalage.info)
  5 +All rights reserved.
  6 +
  7 +Redistribution and use in source and binary forms, with or without modification,
  8 +are permitted provided that the following conditions are met:
  9 +
  10 + * Redistributions of source code must retain the above copyright notice, this
  11 + list of conditions and the following disclaimer.
  12 + * Redistributions in binary form must reproduce the above copyright notice,
  13 + this list of conditions and the following disclaimer in the documentation
  14 + and/or other materials provided with the distribution.
  15 +
  16 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17 +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18 +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  20 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  21 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  22 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  23 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  24 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  25 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
... ...
oletools/__init__.py 0 → 100644
  1 +++ a/oletools/__init__.py
... ...
oletools/ezhexviewer.py 0 → 100644
  1 +++ a/oletools/ezhexviewer.py
  1 +#!/usr/bin/env python
  2 +"""
  3 +ezhexviewer.py
  4 +
  5 +A simple hexadecimal viewer based on easygui. It should work on any platform
  6 +with Python 2.x.
  7 +
  8 +Usage: ezhexviewer.py [file]
  9 +
  10 +Usage in a python application:
  11 +
  12 + import ezhexviewer
  13 + ezhexviewer.hexview_file(filename)
  14 + ezhexviewer.hexview_data(data)
  15 +
  16 +
  17 +ezhexviewer project website: http://www.decalage.info/python/ezhexviewer
  18 +
  19 +ezhexviewer is copyright (c) 2012, Philippe Lagadec (http://www.decalage.info)
  20 +All rights reserved.
  21 +
  22 +Redistribution and use in source and binary forms, with or without modification,
  23 +are 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 +
  31 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  32 +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  33 +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  34 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  35 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  36 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  37 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  39 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  40 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  41 +"""
  42 +
  43 +__version__ = '0.01'
  44 +
  45 +#------------------------------------------------------------------------------
  46 +# CHANGELOG:
  47 +# 2012-09-17 v0.01 PL: - first version
  48 +# 2012-10-04 v0.02 PL: - added license
  49 +
  50 +#------------------------------------------------------------------------------
  51 +# TODO:
  52 +# + options to set title and msg
  53 +
  54 +
  55 +from thirdparty.easygui import easygui
  56 +import sys
  57 +
  58 +#------------------------------------------------------------------------------
  59 +# The following code (hexdump3 only) is a modified version of the hex dumper
  60 +# recipe published on ASPN by Sebastien Keim and Raymond Hattinger under the
  61 +# PSF license. I added the startindex parameter.
  62 +# see http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/142812
  63 +# PSF license: http://docs.python.org/license.html
  64 +# Copyright (c) 2001-2012 Python Software Foundation; All Rights Reserved
  65 +
  66 +FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
  67 +
  68 +def hexdump3(src, length=8, startindex=0):
  69 + """
  70 + Returns a hexadecimal dump of a binary string.
  71 + length: number of bytes per row.
  72 + startindex: index of 1st byte.
  73 + """
  74 + result=[]
  75 + for i in xrange(0, len(src), length):
  76 + s = src[i:i+length]
  77 + hexa = ' '.join(["%02X"%ord(x) for x in s])
  78 + printable = s.translate(FILTER)
  79 + result.append("%08X %-*s %s\n" % (i+startindex, length*3, hexa, printable))
  80 + return ''.join(result)
  81 +
  82 +# end of PSF-licensed code.
  83 +#------------------------------------------------------------------------------
  84 +
  85 +
  86 +def hexview_data (data, msg='', title='ezhexviewer', length=16, startindex=0):
  87 + hex = hexdump3(data, length=length, startindex=startindex)
  88 + easygui.codebox(msg=msg, title=title, text=hex)
  89 +
  90 +
  91 +def hexview_file (filename, msg='', title='ezhexviewer', length=16, startindex=0):
  92 + data = open(filename, 'rb').read()
  93 + hexview_data(data, msg=msg, title=title, length=length, startindex=startindex)
  94 +
  95 +
  96 +if __name__ == '__main__':
  97 + try:
  98 + filename = sys.argv[1]
  99 + except:
  100 + filename = easygui.fileopenbox()
  101 + if filename:
  102 + try:
  103 + hexview_file(filename, msg='File: %s' % filename)
  104 + except:
  105 + easygui.exceptionbox(msg='Error:', title='ezhexviewer')
... ...
oletools/olebrowse.py 0 → 100644
  1 +++ a/oletools/olebrowse.py
  1 +#!/usr/bin/env python
  2 +"""
  3 +olebrowse.py
  4 +
  5 +A simple GUI to browse OLE files (e.g. MS Word, Excel, Powerpoint documents), to
  6 +view and extract individual data streams.
  7 +
  8 +Usage: olebrowse.py [file]
  9 +
  10 +olebrowse project website: http://www.decalage.info/python/olebrowse
  11 +
  12 +olebrowse is copyright (c) 2012, Philippe Lagadec (http://www.decalage.info)
  13 +All rights reserved.
  14 +
  15 +Redistribution and use in source and binary forms, with or without modification,
  16 +are permitted provided that the following conditions are met:
  17 +
  18 + * Redistributions of source code must retain the above copyright notice, this
  19 + list of conditions and the following disclaimer.
  20 + * Redistributions in binary form must reproduce the above copyright notice,
  21 + this list of conditions and the following disclaimer in the documentation
  22 + and/or other materials provided with the distribution.
  23 +
  24 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  25 +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  26 +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  27 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  28 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  29 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  30 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  31 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  32 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  33 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  34 +"""
  35 +
  36 +__version__ = '0.01'
  37 +
  38 +#------------------------------------------------------------------------------
  39 +# CHANGELOG:
  40 +# 2012-09-17 v0.01 PL: - first version
  41 +
  42 +#------------------------------------------------------------------------------
  43 +# TODO:
  44 +# - menu option to open another file
  45 +# - menu option to display properties
  46 +# - menu option to run xxxswf2, oleid, oleyara, olecarve, etc
  47 +# - for a stream, display info: size, path, etc
  48 +# - stream info: magic, entropy, ... ?
  49 +
  50 +import optparse, sys, os
  51 +from thirdparty.easygui import easygui
  52 +from thirdparty.OleFileIO_PL import OleFileIO_PL
  53 +import ezhexviewer
  54 +
  55 +ABOUT = '~ About olebrowse'
  56 +QUIT = '~ Quit'
  57 +
  58 +
  59 +def about ():
  60 + easygui.textbox(title='About olebrowse', text=__doc__)
  61 +
  62 +
  63 +def browse_stream (ole, stream):
  64 + #print 'stream:', stream
  65 + while True:
  66 + msg ='Select an action for the stream "%s", or press Esc to exit' % repr(stream)
  67 + actions = [
  68 + 'Hex view',
  69 +## 'Text view',
  70 +## 'Repr view',
  71 + 'Save stream to file',
  72 + '~ Back to main menu',
  73 + ]
  74 + action = easygui.choicebox(msg, title='olebrowse', choices=actions)
  75 + if action is None or 'Back' in action:
  76 + break
  77 + elif action.startswith('Hex'):
  78 + data = ole.openstream(stream).getvalue()
  79 + ezhexviewer.hexview_data(data, msg='Stream: %s' % stream, title='olebrowse')
  80 +## elif action.startswith('Text'):
  81 +## data = ole.openstream(stream).getvalue()
  82 +## easygui.codebox(title='Text view - %s' % stream, text=data)
  83 +## elif action.startswith('Repr'):
  84 +## data = ole.openstream(stream).getvalue()
  85 +## easygui.codebox(title='Repr view - %s' % stream, text=repr(data))
  86 + elif action.startswith('Save'):
  87 + data = ole.openstream(stream).getvalue()
  88 + fname = easygui.filesavebox(default='stream.bin')
  89 + if fname is not None:
  90 + f = open(fname, 'wb')
  91 + f.write(data)
  92 + f.close()
  93 + easygui.msgbox('stream saved to file %s' % fname)
  94 +
  95 +
  96 +
  97 +def main():
  98 + try:
  99 + filename = sys.argv[1]
  100 + except:
  101 + filename = easygui.fileopenbox()
  102 + try:
  103 + ole = OleFileIO_PL.OleFileIO(filename)
  104 + listdir = ole.listdir()
  105 + streams = []
  106 + for direntry in listdir:
  107 + #print direntry
  108 + streams.append('/'.join(direntry))
  109 + streams.append(ABOUT)
  110 + streams.append(QUIT)
  111 + stream = True
  112 + while stream is not None:
  113 + msg ="Select a stream, or press Esc to exit"
  114 + title = "olebrowse"
  115 + stream = easygui.choicebox(msg, title, streams)
  116 + if stream is None or stream == QUIT:
  117 + break
  118 + if stream == ABOUT:
  119 + about()
  120 + else:
  121 + browse_stream(ole, stream)
  122 + except:
  123 + easygui.exceptionbox()
  124 +
  125 +
  126 +
  127 +
  128 +if __name__ == '__main__':
  129 + main()
... ...
oletools/thirdparty/OleFileIO_PL/LICENSE.txt 0 → 100644
  1 +++ a/oletools/thirdparty/OleFileIO_PL/LICENSE.txt
  1 +LICENSE for the OleFileIO_PL module:
  2 +
  3 +
  4 +OleFileIO_PL is an improved version of the OleFileIO module from the
  5 +Python Imaging Library (PIL).
  6 +
  7 +OleFileIO_PL changes are Copyright (c) 2005-2012 by Philippe Lagadec
  8 +
  9 +The Python Imaging Library (PIL) is
  10 + Copyright (c) 1997-2005 by Secret Labs AB
  11 + Copyright (c) 1995-2005 by Fredrik Lundh
  12 +
  13 +By obtaining, using, and/or copying this software and/or its associated
  14 +documentation, you agree that you have read, understood, and will comply with
  15 +the following terms and conditions:
  16 +
  17 +Permission to use, copy, modify, and distribute this software and its
  18 +associated documentation for any purpose and without fee is hereby granted,
  19 +provided that the above copyright notice appears in all copies, and that both
  20 +that copyright notice and this permission notice appear in supporting
  21 +documentation, and that the name of Secret Labs AB or the author(s) not be used
  22 +in advertising or publicity pertaining to distribution of the software
  23 +without specific, written prior permission.
  24 +
  25 +SECRET LABS AB AND THE AUTHORS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
  26 +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
  27 +IN NO EVENT SHALL SECRET LABS AB OR THE AUTHORS BE LIABLE FOR ANY SPECIAL,
  28 +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  29 +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
  30 +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  31 +PERFORMANCE OF THIS SOFTWARE.
... ...
oletools/thirdparty/OleFileIO_PL/OleFileIO_PL.py 0 → 100644
  1 +++ a/oletools/thirdparty/OleFileIO_PL/OleFileIO_PL.py
  1 +#!/usr/local/bin/python
  2 +# -*- coding: latin-1 -*-
  3 +"""
  4 +OleFileIO_PL:
  5 + Module to read Microsoft OLE2 files (also called Structured Storage or
  6 + Microsoft Compound Document File Format), such as Microsoft Office
  7 + documents, Image Composer and FlashPix files, Outlook messages, ...
  8 +
  9 +version 0.24 2012-09-18 Philippe Lagadec - http://www.decalage.info
  10 +
  11 +Project website: http://www.decalage.info/python/olefileio
  12 +
  13 +Improved version of the OleFileIO module from PIL library v1.1.6
  14 +See: http://www.pythonware.com/products/pil/index.htm
  15 +
  16 +The Python Imaging Library (PIL) is
  17 + Copyright (c) 1997-2005 by Secret Labs AB
  18 + Copyright (c) 1995-2005 by Fredrik Lundh
  19 +OleFileIO_PL changes are Copyright (c) 2005-2012 by Philippe Lagadec
  20 +
  21 +See source code and LICENSE.txt for information on usage and redistribution.
  22 +
  23 +WARNING: THIS IS (STILL) WORK IN PROGRESS.
  24 +"""
  25 +
  26 +__author__ = "Fredrik Lundh (Secret Labs AB), Philippe Lagadec"
  27 +__date__ = "2012-09-18"
  28 +__version__ = '0.24'
  29 +
  30 +#--- LICENSE ------------------------------------------------------------------
  31 +
  32 +# OleFileIO_PL is an improved version of the OleFileIO module from the
  33 +# Python Imaging Library (PIL).
  34 +
  35 +# OleFileIO_PL changes are Copyright (c) 2005-2012 by Philippe Lagadec
  36 +#
  37 +# The Python Imaging Library (PIL) is
  38 +# Copyright (c) 1997-2005 by Secret Labs AB
  39 +# Copyright (c) 1995-2005 by Fredrik Lundh
  40 +#
  41 +# By obtaining, using, and/or copying this software and/or its associated
  42 +# documentation, you agree that you have read, understood, and will comply with
  43 +# the following terms and conditions:
  44 +#
  45 +# Permission to use, copy, modify, and distribute this software and its
  46 +# associated documentation for any purpose and without fee is hereby granted,
  47 +# provided that the above copyright notice appears in all copies, and that both
  48 +# that copyright notice and this permission notice appear in supporting
  49 +# documentation, and that the name of Secret Labs AB or the author(s) not be used
  50 +# in advertising or publicity pertaining to distribution of the software
  51 +# without specific, written prior permission.
  52 +#
  53 +# SECRET LABS AB AND THE AUTHORS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
  54 +# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
  55 +# IN NO EVENT SHALL SECRET LABS AB OR THE AUTHORS BE LIABLE FOR ANY SPECIAL,
  56 +# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  57 +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
  58 +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  59 +# PERFORMANCE OF THIS SOFTWARE.
  60 +
  61 +#-----------------------------------------------------------------------------
  62 +# CHANGELOG: (only OleFileIO_PL changes compared to PIL 1.1.6)
  63 +# 2005-05-11 v0.10 PL: - a few fixes for Python 2.4 compatibility
  64 +# (all changes flagged with [PL])
  65 +# 2006-02-22 v0.11 PL: - a few fixes for some Office 2003 documents which raise
  66 +# exceptions in _OleStream.__init__()
  67 +# 2006-06-09 v0.12 PL: - fixes for files above 6.8MB (DIFAT in loadfat)
  68 +# - added some constants
  69 +# - added header values checks
  70 +# - added some docstrings
  71 +# - getsect: bugfix in case sectors >512 bytes
  72 +# - getsect: added conformity checks
  73 +# - DEBUG_MODE constant to activate debug display
  74 +# 2007-09-04 v0.13 PL: - improved/translated (lots of) comments
  75 +# - updated license
  76 +# - converted tabs to 4 spaces
  77 +# 2007-11-19 v0.14 PL: - added OleFileIO._raise_defect() to adapt sensitivity
  78 +# - improved _unicode() to use Python 2.x unicode support
  79 +# - fixed bug in _OleDirectoryEntry
  80 +# 2007-11-25 v0.15 PL: - added safety checks to detect FAT loops
  81 +# - fixed _OleStream which didn't check stream size
  82 +# - added/improved many docstrings and comments
  83 +# - moved helper functions _unicode and _clsid out of
  84 +# OleFileIO class
  85 +# - improved OleFileIO._find() to add Unix path syntax
  86 +# - OleFileIO._find() is now case-insensitive
  87 +# - added get_type() and get_rootentry_name()
  88 +# - rewritten loaddirectory and _OleDirectoryEntry
  89 +# 2007-11-27 v0.16 PL: - added _OleDirectoryEntry.kids_dict
  90 +# - added detection of duplicate filenames in storages
  91 +# - added detection of duplicate references to streams
  92 +# - added get_size() and exists() to _OleDirectoryEntry
  93 +# - added isOleFile to check header before parsing
  94 +# - added __all__ list to control public keywords in pydoc
  95 +# 2007-12-04 v0.17 PL: - added _load_direntry to fix a bug in loaddirectory
  96 +# - improved _unicode(), added workarounds for Python <2.3
  97 +# - added set_debug_mode and -d option to set debug mode
  98 +# - fixed bugs in OleFileIO.open and _OleDirectoryEntry
  99 +# - added safety check in main for large or binary
  100 +# properties
  101 +# - allow size>0 for storages for some implementations
  102 +# 2007-12-05 v0.18 PL: - fixed several bugs in handling of FAT, MiniFAT and
  103 +# streams
  104 +# - added option '-c' in main to check all streams
  105 +# 2009-12-10 v0.19 PL: - bugfix for 32 bit arrays on 64 bits platforms
  106 +# (thanks to Ben G. and Martijn for reporting the bug)
  107 +# 2009-12-11 v0.20 PL: - bugfix in OleFileIO.open when filename is not plain str
  108 +# 2010-01-22 v0.21 PL: - added support for big-endian CPUs such as PowerPC Macs
  109 +# 2012-02-16 v0.22 PL: - fixed bug in getproperties, patch by chuckleberryfinn
  110 +# (https://bitbucket.org/decalage/olefileio_pl/issue/7)
  111 +# - added close method to OleFileIO (fixed issue #2)
  112 +# 2012-07-25 v0.23 PL: - added support for file-like objects (patch by mete0r_kr)
  113 +
  114 +
  115 +#-----------------------------------------------------------------------------
  116 +# TODO (for version 1.0):
  117 +# + add path attrib to _OleDirEntry, set it once and for all in init or
  118 +# append_kids (then listdir/_list can be simplified)
  119 +# - TESTS with Linux, MacOSX, Python 1.5.2, various files, PIL, ...
  120 +# - add underscore to each private method, to avoid their display in
  121 +# pydoc/epydoc documentation - Remove it for classes to be documented
  122 +# - replace all raised exceptions with _raise_defect (at least in OleFileIO)
  123 +# - merge code from _OleStream and OleFileIO.getsect to read sectors
  124 +# (maybe add a class for FAT and MiniFAT ?)
  125 +# - add method to check all streams (follow sectors chains without storing all
  126 +# stream in memory, and report anomalies)
  127 +# - use _OleDirectoryEntry.kids_dict to improve _find and _list ?
  128 +# - fix Unicode names handling (find some way to stay compatible with Py1.5.2)
  129 +# => if possible avoid converting names to Latin-1
  130 +# - review DIFAT code: fix handling of DIFSECT blocks in FAT (not stop)
  131 +# - rewrite OleFileIO.getproperties
  132 +# - improve docstrings to show more sample uses
  133 +# - see also original notes and FIXME below
  134 +# - remove all obsolete FIXMEs
  135 +
  136 +# IDEAS:
  137 +# - allow _raise_defect to raise different exceptions, not only IOError
  138 +# - provide a class with named attributes to get well-known properties of
  139 +# MS Office documents (title, author, ...) ?
  140 +# - in OleFileIO._open and _OleStream, use size=None instead of 0x7FFFFFFF for
  141 +# streams with unknown size
  142 +# - use arrays of int instead of long integers for FAT/MiniFAT, to improve
  143 +# performance and reduce memory usage ? (possible issue with values >2^31)
  144 +# - provide tests with unittest (may need write support to create samples)
  145 +# - move all debug code (and maybe dump methods) to a separate module, with
  146 +# a class which inherits OleFileIO ?
  147 +# - fix docstrings to follow epydoc format
  148 +# - add support for 4K sectors ?
  149 +# - add support for big endian byte order ?
  150 +# - create a simple OLE explorer with wxPython
  151 +
  152 +# FUTURE EVOLUTIONS to add write support:
  153 +# 1) add ability to write a stream back on disk from StringIO (same size, no
  154 +# change in FAT/MiniFAT).
  155 +# 2) rename a stream/storage if it doesn't change the RB tree
  156 +# 3) use rbtree module to update the red-black tree + any rename
  157 +# 4) remove a stream/storage: free sectors in FAT/MiniFAT
  158 +# 5) allocate new sectors in FAT/MiniFAT
  159 +# 6) create new storage/stream
  160 +#-----------------------------------------------------------------------------
  161 +
  162 +#
  163 +# THIS IS WORK IN PROGRESS
  164 +#
  165 +# The Python Imaging Library
  166 +# $Id: OleFileIO.py 2339 2005-03-25 08:02:17Z fredrik $
  167 +#
  168 +# stuff to deal with OLE2 Structured Storage files. this module is
  169 +# used by PIL to read Image Composer and FlashPix files, but can also
  170 +# be used to read other files of this type.
  171 +#
  172 +# History:
  173 +# 1997-01-20 fl Created
  174 +# 1997-01-22 fl Fixed 64-bit portability quirk
  175 +# 2003-09-09 fl Fixed typo in OleFileIO.loadfat (noted by Daniel Haertle)
  176 +# 2004-02-29 fl Changed long hex constants to signed integers
  177 +#
  178 +# Notes:
  179 +# FIXME: sort out sign problem (eliminate long hex constants)
  180 +# FIXME: change filename to use "a/b/c" instead of ["a", "b", "c"]
  181 +# FIXME: provide a glob mechanism function (using fnmatchcase)
  182 +#
  183 +# Literature:
  184 +#
  185 +# "FlashPix Format Specification, Appendix A", Kodak and Microsoft,
  186 +# September 1996.
  187 +#
  188 +# Quotes:
  189 +#
  190 +# "If this document and functionality of the Software conflict,
  191 +# the actual functionality of the Software represents the correct
  192 +# functionality" -- Microsoft, in the OLE format specification
  193 +#
  194 +# Copyright (c) Secret Labs AB 1997.
  195 +# Copyright (c) Fredrik Lundh 1997.
  196 +#
  197 +# See the README file for information on usage and redistribution.
  198 +#
  199 +
  200 +#------------------------------------------------------------------------------
  201 +
  202 +import string, StringIO, struct, array, os.path, sys
  203 +
  204 +#[PL] Define explicitly the public API to avoid private objects in pydoc:
  205 +__all__ = ['OleFileIO', 'isOleFile']
  206 +
  207 +#[PL] workaround to fix an issue with array item size on 64 bits systems:
  208 +if array.array('L').itemsize == 4:
  209 + # on 32 bits platforms, long integers in an array are 32 bits:
  210 + UINT32 = 'L'
  211 +elif array.array('I').itemsize == 4:
  212 + # on 64 bits platforms, integers in an array are 32 bits:
  213 + UINT32 = 'I'
  214 +else:
  215 + raise ValueError, 'Need to fix a bug with 32 bit arrays, please contact author...'
  216 +
  217 +
  218 +#[PL] These workarounds were inspired from the Path module
  219 +# (see http://www.jorendorff.com/articles/python/path/)
  220 +#TODO: test with old Python versions
  221 +
  222 +# Pre-2.3 workaround for booleans
  223 +try:
  224 + True, False
  225 +except NameError:
  226 + True, False = 1, 0
  227 +
  228 +# Pre-2.3 workaround for basestring.
  229 +try:
  230 + basestring
  231 +except NameError:
  232 + try:
  233 + # is Unicode supported (Python >2.0 or >1.6 ?)
  234 + basestring = (str, unicode)
  235 + except NameError:
  236 + basestring = str
  237 +
  238 +#[PL] Experimental setting: if True, OLE filenames will be kept in Unicode
  239 +# if False (default PIL behaviour), all filenames are converted to Latin-1.
  240 +KEEP_UNICODE_NAMES = False
  241 +
  242 +#[PL] DEBUG display mode: False by default, use set_debug_mode() or "-d" on
  243 +# command line to change it.
  244 +DEBUG_MODE = False
  245 +def debug_print(msg):
  246 + print msg
  247 +def debug_pass(msg):
  248 + pass
  249 +debug = debug_pass
  250 +
  251 +def set_debug_mode(debug_mode):
  252 + """
  253 + Set debug mode on or off, to control display of debugging messages.
  254 + mode: True or False
  255 + """
  256 + global DEBUG_MODE, debug
  257 + DEBUG_MODE = debug_mode
  258 + if debug_mode:
  259 + debug = debug_print
  260 + else:
  261 + debug = debug_pass
  262 +
  263 +#TODO: convert this to hex
  264 +MAGIC = '\320\317\021\340\241\261\032\341'
  265 +
  266 +#[PL]: added constants for Sector IDs (from AAF specifications)
  267 +MAXREGSECT = 0xFFFFFFFAL; # maximum SECT
  268 +DIFSECT = 0xFFFFFFFCL; # (-4) denotes a DIFAT sector in a FAT
  269 +FATSECT = 0xFFFFFFFDL; # (-3) denotes a FAT sector in a FAT
  270 +ENDOFCHAIN = 0xFFFFFFFEL; # (-2) end of a virtual stream chain
  271 +FREESECT = 0xFFFFFFFFL; # (-1) unallocated sector
  272 +
  273 +#[PL]: added constants for Directory Entry IDs (from AAF specifications)
  274 +MAXREGSID = 0xFFFFFFFAL; # maximum directory entry ID
  275 +NOSTREAM = 0xFFFFFFFFL; # (-1) unallocated directory entry
  276 +
  277 +#[PL] object types in storage (from AAF specifications)
  278 +STGTY_EMPTY = 0 # empty directory entry (according to OpenOffice.org doc)
  279 +STGTY_STORAGE = 1 # element is a storage object
  280 +STGTY_STREAM = 2 # element is a stream object
  281 +STGTY_LOCKBYTES = 3 # element is an ILockBytes object
  282 +STGTY_PROPERTY = 4 # element is an IPropertyStorage object
  283 +STGTY_ROOT = 5 # element is a root storage
  284 +
  285 +
  286 +#
  287 +# --------------------------------------------------------------------
  288 +# property types
  289 +
  290 +VT_EMPTY=0; VT_NULL=1; VT_I2=2; VT_I4=3; VT_R4=4; VT_R8=5; VT_CY=6;
  291 +VT_DATE=7; VT_BSTR=8; VT_DISPATCH=9; VT_ERROR=10; VT_BOOL=11;
  292 +VT_VARIANT=12; VT_UNKNOWN=13; VT_DECIMAL=14; VT_I1=16; VT_UI1=17;
  293 +VT_UI2=18; VT_UI4=19; VT_I8=20; VT_UI8=21; VT_INT=22; VT_UINT=23;
  294 +VT_VOID=24; VT_HRESULT=25; VT_PTR=26; VT_SAFEARRAY=27; VT_CARRAY=28;
  295 +VT_USERDEFINED=29; VT_LPSTR=30; VT_LPWSTR=31; VT_FILETIME=64;
  296 +VT_BLOB=65; VT_STREAM=66; VT_STORAGE=67; VT_STREAMED_OBJECT=68;
  297 +VT_STORED_OBJECT=69; VT_BLOB_OBJECT=70; VT_CF=71; VT_CLSID=72;
  298 +VT_VECTOR=0x1000;
  299 +
  300 +# map property id to name (for debugging purposes)
  301 +
  302 +VT = {}
  303 +for keyword, var in vars().items():
  304 + if keyword[:3] == "VT_":
  305 + VT[var] = keyword
  306 +
  307 +#
  308 +# --------------------------------------------------------------------
  309 +# Some common document types (root.clsid fields)
  310 +
  311 +WORD_CLSID = "00020900-0000-0000-C000-000000000046"
  312 +#TODO: check Excel, PPT, ...
  313 +
  314 +#[PL]: Defect levels to classify parsing errors - see OleFileIO._raise_defect()
  315 +DEFECT_UNSURE = 10 # a case which looks weird, but not sure it's a defect
  316 +DEFECT_POTENTIAL = 20 # a potential defect
  317 +DEFECT_INCORRECT = 30 # an error according to specifications, but parsing
  318 + # can go on
  319 +DEFECT_FATAL = 40 # an error which cannot be ignored, parsing is
  320 + # impossible
  321 +
  322 +#[PL] add useful constants to __all__:
  323 +for key in vars().keys():
  324 + if key.startswith('STGTY_') or key.startswith('DEFECT_'):
  325 + __all__.append(key)
  326 +
  327 +
  328 +#--- FUNCTIONS ----------------------------------------------------------------
  329 +
  330 +def isOleFile (filename):
  331 + """
  332 + Test if file is an OLE container (according to its header).
  333 + filename: file name or path (str, unicode)
  334 + return: True if OLE, False otherwise.
  335 + """
  336 + f = open(filename, 'rb')
  337 + header = f.read(len(MAGIC))
  338 + if header == MAGIC:
  339 + return True
  340 + else:
  341 + return False
  342 +
  343 +
  344 +#TODO: replace i16 and i32 with more readable struct.unpack equivalent
  345 +def i16(c, o = 0):
  346 + """
  347 + Converts a 2-bytes (16 bits) string to an integer.
  348 +
  349 + c: string containing bytes to convert
  350 + o: offset of bytes to convert in string
  351 + """
  352 + return ord(c[o])+(ord(c[o+1])<<8)
  353 +
  354 +
  355 +def i32(c, o = 0):
  356 + """
  357 + Converts a 4-bytes (32 bits) string to an integer.
  358 +
  359 + c: string containing bytes to convert
  360 + o: offset of bytes to convert in string
  361 + """
  362 + return int(ord(c[o])+(ord(c[o+1])<<8)+(ord(c[o+2])<<16)+(ord(c[o+3])<<24))
  363 + # [PL]: added int() because "<<" gives long int since Python 2.4
  364 +
  365 +
  366 +def _clsid(clsid):
  367 + """
  368 + Converts a CLSID to a human-readable string.
  369 + clsid: string of length 16.
  370 + """
  371 + assert len(clsid) == 16
  372 + if clsid == "\0" * len(clsid):
  373 + return ""
  374 + return (("%08X-%04X-%04X-%02X%02X-" + "%02X" * 6) %
  375 + ((i32(clsid, 0), i16(clsid, 4), i16(clsid, 6)) +
  376 + tuple(map(ord, clsid[8:16]))))
  377 +
  378 +
  379 +
  380 +# UNICODE support for Old Python versions:
  381 +# (necessary to handle storages/streams names which use Unicode)
  382 +
  383 +try:
  384 + # is Unicode supported ?
  385 + unicode
  386 +
  387 + def _unicode(s, errors='replace'):
  388 + """
  389 + Map unicode string to Latin 1. (Python with Unicode support)
  390 +
  391 + s: UTF-16LE unicode string to convert to Latin-1
  392 + errors: 'replace', 'ignore' or 'strict'. See Python doc for unicode()
  393 + """
  394 + #TODO: test if it OleFileIO works with Unicode strings, instead of
  395 + # converting to Latin-1.
  396 + try:
  397 + # First the string is converted to plain Unicode:
  398 + # (assuming it is encoded as UTF-16 little-endian)
  399 + u = s.decode('UTF-16LE', errors)
  400 + if KEEP_UNICODE_NAMES:
  401 + return u
  402 + else:
  403 + # Second the unicode string is converted to Latin-1
  404 + return u.encode('latin_1', errors)
  405 + except:
  406 + # there was an error during Unicode to Latin-1 conversion:
  407 + raise IOError, 'incorrect Unicode name'
  408 +
  409 +except NameError:
  410 + def _unicode(s, errors='replace'):
  411 + """
  412 + Map unicode string to Latin 1. (Python without native Unicode support)
  413 +
  414 + s: UTF-16LE unicode string to convert to Latin-1
  415 + errors: 'replace', 'ignore' or 'strict'. (ignored in this version)
  416 + """
  417 + # If the unicode function does not exist, we assume this is an old
  418 + # Python version without Unicode support.
  419 + # Null bytes are simply removed (this only works with usual Latin-1
  420 + # strings which do not contain unicode characters>256):
  421 + return filter(ord, s)
  422 +
  423 +
  424 +
  425 +
  426 +#=== CLASSES ==================================================================
  427 +
  428 +#--- _OleStream ---------------------------------------------------------------
  429 +
  430 +class _OleStream(StringIO.StringIO):
  431 + """
  432 + OLE2 Stream
  433 +
  434 + Returns a read-only file object which can be used to read
  435 + the contents of a OLE stream (instance of the StringIO class).
  436 + To open a stream, use the openstream method in the OleFile class.
  437 +
  438 + This function can be used with either ordinary streams,
  439 + or ministreams, depending on the offset, sectorsize, and
  440 + fat table arguments.
  441 +
  442 + Attributes:
  443 + - size: actual size of data stream, after it was opened.
  444 + """
  445 +
  446 + # FIXME: should store the list of sects obtained by following
  447 + # the fat chain, and load new sectors on demand instead of
  448 + # loading it all in one go.
  449 +
  450 + def __init__(self, fp, sect, size, offset, sectorsize, fat, filesize):
  451 + """
  452 + Constructor for _OleStream class.
  453 +
  454 + fp : file object, the OLE container or the MiniFAT stream
  455 + sect : sector index of first sector in the stream
  456 + size : total size of the stream
  457 + offset : offset in bytes for the first FAT or MiniFAT sector
  458 + sectorsize: size of one sector
  459 + fat : array/list of sector indexes (FAT or MiniFAT)
  460 + filesize : size of OLE file (for debugging)
  461 + return : a StringIO instance containing the OLE stream
  462 + """
  463 + debug('_OleStream.__init__:')
  464 + debug(' sect=%d (%X), size=%d, offset=%d, sectorsize=%d, len(fat)=%d, fp=%s'
  465 + %(sect,sect,size,offset,sectorsize,len(fat), repr(fp)))
  466 + #[PL] To detect malformed documents with FAT loops, we compute the
  467 + # expected number of sectors in the stream:
  468 + unknown_size = False
  469 + if size==0x7FFFFFFF:
  470 + # this is the case when called from OleFileIO._open(), and stream
  471 + # size is not known in advance (for example when reading the
  472 + # Directory stream). Then we can only guess maximum size:
  473 + size = len(fat)*sectorsize
  474 + # and we keep a record that size was unknown:
  475 + unknown_size = True
  476 + debug(' stream with UNKNOWN SIZE')
  477 + nb_sectors = (size + (sectorsize-1)) / sectorsize
  478 + debug('nb_sectors = %d' % nb_sectors)
  479 + # This number should (at least) be less than the total number of
  480 + # sectors in the given FAT:
  481 + if nb_sectors > len(fat):
  482 + raise IOError, 'malformed OLE document, stream too large'
  483 + # optimization(?): data is first a list of strings, and join() is called
  484 + # at the end to concatenate all in one string.
  485 + # (this may not be really useful with recent Python versions)
  486 + data = []
  487 + # if size is zero, then first sector index should be ENDOFCHAIN:
  488 + if size == 0 and sect != ENDOFCHAIN:
  489 + debug('size == 0 and sect != ENDOFCHAIN:')
  490 + raise IOError, 'incorrect OLE sector index for empty stream'
  491 + #[PL] A fixed-length for loop is used instead of an undefined while
  492 + # loop to avoid DoS attacks:
  493 + for i in xrange(nb_sectors):
  494 + # Sector index may be ENDOFCHAIN, but only if size was unknown
  495 + if sect == ENDOFCHAIN:
  496 + if unknown_size:
  497 + break
  498 + else:
  499 + # else this means that the stream is smaller than declared:
  500 + debug('sect=ENDOFCHAIN before expected size')
  501 + raise IOError, 'incomplete OLE stream'
  502 + # sector index should be within FAT:
  503 + if sect<0 or sect>=len(fat):
  504 + debug('sect=%d (%X) / len(fat)=%d' % (sect, sect, len(fat)))
  505 + debug('i=%d / nb_sectors=%d' %(i, nb_sectors))
  506 +## tmp_data = string.join(data, "")
  507 +## f = open('test_debug.bin', 'wb')
  508 +## f.write(tmp_data)
  509 +## f.close()
  510 +## debug('data read so far: %d bytes' % len(tmp_data))
  511 + raise IOError, 'incorrect OLE FAT, sector index out of range'
  512 + #TODO: merge this code with OleFileIO.getsect() ?
  513 + #TODO: check if this works with 4K sectors:
  514 + try:
  515 + fp.seek(offset + sectorsize * sect)
  516 + except:
  517 + debug('sect=%d, seek=%d, filesize=%d' %
  518 + (sect, offset+sectorsize*sect, filesize))
  519 + raise IOError, 'OLE sector index out of range'
  520 + sector_data = fp.read(sectorsize)
  521 + # [PL] check if there was enough data:
  522 + # Note: if sector is the last of the file, sometimes it is not a
  523 + # complete sector (of 512 or 4K), so we may read less than
  524 + # sectorsize.
  525 + if len(sector_data)!=sectorsize and sect!=(len(fat)-1):
  526 + debug('sect=%d / len(fat)=%d, seek=%d / filesize=%d, len read=%d' %
  527 + (sect, len(fat), offset+sectorsize*sect, filesize, len(sector_data)))
  528 + debug('seek+len(read)=%d' % (offset+sectorsize*sect+len(sector_data)))
  529 + raise IOError, 'incomplete OLE sector'
  530 + data.append(sector_data)
  531 + # jump to next sector in the FAT:
  532 + try:
  533 + sect = fat[sect]
  534 + except IndexError:
  535 + # [PL] if pointer is out of the FAT an exception is raised
  536 + raise IOError, 'incorrect OLE FAT, sector index out of range'
  537 + #[PL] Last sector should be a "end of chain" marker:
  538 + if sect != ENDOFCHAIN:
  539 + raise IOError, 'incorrect last sector index in OLE stream'
  540 + data = string.join(data, "")
  541 + # Data is truncated to the actual stream size:
  542 + if len(data) >= size:
  543 + data = data[:size]
  544 + # actual stream size is stored for future use:
  545 + self.size = size
  546 + elif unknown_size:
  547 + # actual stream size was not known, now we know the size of read
  548 + # data:
  549 + self.size = len(data)
  550 + else:
  551 + # read data is less than expected:
  552 + debug('len(data)=%d, size=%d' % (len(data), size))
  553 + raise IOError, 'OLE stream size is less than declared'
  554 + # when all data is read in memory, StringIO constructor is called
  555 + StringIO.StringIO.__init__(self, data)
  556 + # Then the _OleStream object can be used as a read-only file object.
  557 +
  558 +
  559 +#--- _OleDirectoryEntry -------------------------------------------------------
  560 +
  561 +class _OleDirectoryEntry:
  562 +
  563 + """
  564 + OLE2 Directory Entry
  565 + """
  566 + #[PL] parsing code moved from OleFileIO.loaddirectory
  567 +
  568 + # struct to parse directory entries:
  569 + # <: little-endian byte order
  570 + # 64s: string containing entry name in unicode (max 31 chars) + null char
  571 + # H: uint16, number of bytes used in name buffer, including null = (len+1)*2
  572 + # B: uint8, dir entry type (between 0 and 5)
  573 + # B: uint8, color: 0=black, 1=red
  574 + # I: uint32, index of left child node in the red-black tree, NOSTREAM if none
  575 + # I: uint32, index of right child node in the red-black tree, NOSTREAM if none
  576 + # I: uint32, index of child root node if it is a storage, else NOSTREAM
  577 + # 16s: CLSID, unique identifier (only used if it is a storage)
  578 + # I: uint32, user flags
  579 + # 8s: uint64, creation timestamp or zero
  580 + # 8s: uint64, modification timestamp or zero
  581 + # I: uint32, SID of first sector if stream or ministream, SID of 1st sector
  582 + # of stream containing ministreams if root entry, 0 otherwise
  583 + # I: uint32, total stream size in bytes if stream (low 32 bits), 0 otherwise
  584 + # I: uint32, total stream size in bytes if stream (high 32 bits), 0 otherwise
  585 + STRUCT_DIRENTRY = '<64sHBBIII16sI8s8sIII'
  586 + # size of a directory entry: 128 bytes
  587 + DIRENTRY_SIZE = 128
  588 + assert struct.calcsize(STRUCT_DIRENTRY) == DIRENTRY_SIZE
  589 +
  590 +
  591 + def __init__(self, entry, sid, olefile):
  592 + """
  593 + Constructor for an _OleDirectoryEntry object.
  594 + Parses a 128-bytes entry from the OLE Directory stream.
  595 +
  596 + entry : string (must be 128 bytes long)
  597 + sid : index of this directory entry in the OLE file directory
  598 + olefile: OleFileIO containing this directory entry
  599 + """
  600 + self.sid = sid
  601 + # ref to olefile is stored for future use
  602 + self.olefile = olefile
  603 + # kids is a list of children entries, if this entry is a storage:
  604 + # (list of _OleDirectoryEntry objects)
  605 + self.kids = []
  606 + # kids_dict is a dictionary of children entries, indexed by their
  607 + # name in lowercase: used to quickly find an entry, and to detect
  608 + # duplicates
  609 + self.kids_dict = {}
  610 + # flag used to detect if the entry is referenced more than once in
  611 + # directory:
  612 + self.used = False
  613 + # decode DirEntry
  614 + (
  615 + name,
  616 + namelength,
  617 + self.entry_type,
  618 + self.color,
  619 + self.sid_left,
  620 + self.sid_right,
  621 + self.sid_child,
  622 + clsid,
  623 + self.dwUserFlags,
  624 + self.createTime,
  625 + self.modifyTime,
  626 + self.isectStart,
  627 + sizeLow,
  628 + sizeHigh
  629 + ) = struct.unpack(_OleDirectoryEntry.STRUCT_DIRENTRY, entry)
  630 + if self.entry_type not in [STGTY_ROOT, STGTY_STORAGE, STGTY_STREAM, STGTY_EMPTY]:
  631 + olefile._raise_defect(DEFECT_INCORRECT, 'unhandled OLE storage type')
  632 + # only first directory entry can (and should) be root:
  633 + if self.entry_type == STGTY_ROOT and sid != 0:
  634 + olefile._raise_defect(DEFECT_INCORRECT, 'duplicate OLE root entry')
  635 + if sid == 0 and self.entry_type != STGTY_ROOT:
  636 + olefile._raise_defect(DEFECT_INCORRECT, 'incorrect OLE root entry')
  637 + #debug (struct.unpack(fmt_entry, entry[:len_entry]))
  638 + # name should be at most 31 unicode characters + null character,
  639 + # so 64 bytes in total (31*2 + 2):
  640 + if namelength>64:
  641 + olefile._raise_defect(DEFECT_INCORRECT, 'incorrect DirEntry name length')
  642 + # if exception not raised, namelength is set to the maximum value:
  643 + namelength = 64
  644 + # only characters without ending null char are kept:
  645 + name = name[:(namelength-2)]
  646 + # name is converted from unicode to Latin-1:
  647 + self.name = _unicode(name)
  648 +
  649 + debug('DirEntry SID=%d: %s' % (self.sid, repr(self.name)))
  650 + debug(' - type: %d' % self.entry_type)
  651 + debug(' - sect: %d' % self.isectStart)
  652 + debug(' - SID left: %d, right: %d, child: %d' % (self.sid_left,
  653 + self.sid_right, self.sid_child))
  654 +
  655 + # sizeHigh is only used for 4K sectors, it should be zero for 512 bytes
  656 + # sectors, BUT apparently some implementations set it as 0xFFFFFFFFL, 1
  657 + # or some other value so it cannot be raised as a defect in general:
  658 + if olefile.sectorsize == 512:
  659 + if sizeHigh != 0 and sizeHigh != 0xFFFFFFFFL:
  660 + debug('sectorsize=%d, sizeLow=%d, sizeHigh=%d (%X)' %
  661 + (olefile.sectorsize, sizeLow, sizeHigh, sizeHigh))
  662 + olefile._raise_defect(DEFECT_UNSURE, 'incorrect OLE stream size')
  663 + self.size = sizeLow
  664 + else:
  665 + self.size = sizeLow + (long(sizeHigh)<<32)
  666 + debug(' - size: %d (sizeLow=%d, sizeHigh=%d)' % (self.size, sizeLow, sizeHigh))
  667 +
  668 + self.clsid = _clsid(clsid)
  669 + # a storage should have a null size, BUT some implementations such as
  670 + # Word 8 for Mac seem to allow non-null values => Potential defect:
  671 + if self.entry_type == STGTY_STORAGE and self.size != 0:
  672 + olefile._raise_defect(DEFECT_POTENTIAL, 'OLE storage with size>0')
  673 + # check if stream is not already referenced elsewhere:
  674 + if self.entry_type in (STGTY_ROOT, STGTY_STREAM) and self.size>0:
  675 + if self.size < olefile.minisectorcutoff \
  676 + and self.entry_type==STGTY_STREAM: # only streams can be in MiniFAT
  677 + # ministream object
  678 + minifat = True
  679 + else:
  680 + minifat = False
  681 + olefile._check_duplicate_stream(self.isectStart, minifat)
  682 +
  683 +
  684 +
  685 + def build_storage_tree(self):
  686 + """
  687 + Read and build the red-black tree attached to this _OleDirectoryEntry
  688 + object, if it is a storage.
  689 + Note that this method builds a tree of all subentries, so it should
  690 + only be called for the root object once.
  691 + """
  692 + debug('build_storage_tree: SID=%d - %s - sid_child=%d'
  693 + % (self.sid, repr(self.name), self.sid_child))
  694 + if self.sid_child != NOSTREAM:
  695 + # if child SID is not NOSTREAM, then this entry is a storage.
  696 + # Let's walk through the tree of children to fill the kids list:
  697 + self.append_kids(self.sid_child)
  698 +
  699 + # Note from OpenOffice documentation: the safest way is to
  700 + # recreate the tree because some implementations may store broken
  701 + # red-black trees...
  702 +
  703 + # in the OLE file, entries are sorted on (length, name).
  704 + # for convenience, we sort them on name instead:
  705 + # (see __cmp__ method in this class)
  706 + self.kids.sort()
  707 +
  708 +
  709 + def append_kids(self, child_sid):
  710 + """
  711 + Walk through red-black tree of children of this directory entry to add
  712 + all of them to the kids list. (recursive method)
  713 +
  714 + child_sid : index of child directory entry to use, or None when called
  715 + first time for the root. (only used during recursion)
  716 + """
  717 + #[PL] this method was added to use simple recursion instead of a complex
  718 + # algorithm.
  719 + # if this is not a storage or a leaf of the tree, nothing to do:
  720 + if child_sid == NOSTREAM:
  721 + return
  722 + # check if child SID is in the proper range:
  723 + if child_sid<0 or child_sid>=len(self.olefile.direntries):
  724 + self.olefile._raise_defect(DEFECT_FATAL, 'OLE DirEntry index out of range')
  725 + # get child direntry:
  726 + child = self.olefile._load_direntry(child_sid) #direntries[child_sid]
  727 + debug('append_kids: child_sid=%d - %s - sid_left=%d, sid_right=%d, sid_child=%d'
  728 + % (child.sid, repr(child.name), child.sid_left, child.sid_right, child.sid_child))
  729 + # the directory entries are organized as a red-black tree.
  730 + # (cf. Wikipedia for details)
  731 + # First walk through left side of the tree:
  732 + self.append_kids(child.sid_left)
  733 + # Check if its name is not already used (case-insensitive):
  734 + name_lower = child.name.lower()
  735 + if self.kids_dict.has_key(name_lower):
  736 + self.olefile._raise_defect(DEFECT_INCORRECT,
  737 + "Duplicate filename in OLE storage")
  738 + # Then the child_sid _OleDirectoryEntry object is appended to the
  739 + # kids list and dictionary:
  740 + self.kids.append(child)
  741 + self.kids_dict[name_lower] = child
  742 + # Check if kid was not already referenced in a storage:
  743 + if child.used:
  744 + self.olefile._raise_defect(DEFECT_INCORRECT,
  745 + 'OLE Entry referenced more than once')
  746 + child.used = True
  747 + # Finally walk through right side of the tree:
  748 + self.append_kids(child.sid_right)
  749 + # Afterwards build kid's own tree if it's also a storage:
  750 + child.build_storage_tree()
  751 +
  752 +
  753 + def __cmp__(self, other):
  754 + "Compare entries by name"
  755 + return cmp(self.name, other.name)
  756 + #TODO: replace by the same function as MS implementation ?
  757 + # (order by name length first, then case-insensitive order)
  758 +
  759 +
  760 + def dump(self, tab = 0):
  761 + "Dump this entry, and all its subentries (for debug purposes only)"
  762 + TYPES = ["(invalid)", "(storage)", "(stream)", "(lockbytes)",
  763 + "(property)", "(root)"]
  764 + print " "*tab + repr(self.name), TYPES[self.entry_type],
  765 + if self.entry_type in (STGTY_STREAM, STGTY_ROOT):
  766 + print self.size, "bytes",
  767 + print
  768 + if self.entry_type in (STGTY_STORAGE, STGTY_ROOT) and self.clsid:
  769 + print " "*tab + "{%s}" % self.clsid
  770 +
  771 + for kid in self.kids:
  772 + kid.dump(tab + 2)
  773 +
  774 +
  775 +#--- OleFileIO ----------------------------------------------------------------
  776 +
  777 +class OleFileIO:
  778 + """
  779 + OLE container object
  780 +
  781 + This class encapsulates the interface to an OLE 2 structured
  782 + storage file. Use the {@link listdir} and {@link openstream} methods to
  783 + access the contents of this file.
  784 +
  785 + Object names are given as a list of strings, one for each subentry
  786 + level. The root entry should be omitted. For example, the following
  787 + code extracts all image streams from a Microsoft Image Composer file:
  788 +
  789 + ole = OleFileIO("fan.mic")
  790 +
  791 + for entry in ole.listdir():
  792 + if entry[1:2] == "Image":
  793 + fin = ole.openstream(entry)
  794 + fout = open(entry[0:1], "wb")
  795 + while 1:
  796 + s = fin.read(8192)
  797 + if not s:
  798 + break
  799 + fout.write(s)
  800 +
  801 + You can use the viewer application provided with the Python Imaging
  802 + Library to view the resulting files (which happens to be standard
  803 + TIFF files).
  804 + """
  805 +
  806 + def __init__(self, filename = None, raise_defects=DEFECT_FATAL):
  807 + """
  808 + Constructor for OleFileIO class.
  809 +
  810 + filename: file to open.
  811 + raise_defects: minimal level for defects to be raised as exceptions.
  812 + (use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a
  813 + security-oriented application, see source code for details)
  814 + """
  815 + self._raise_defects_level = raise_defects
  816 + if filename:
  817 + self.open(filename)
  818 +
  819 +
  820 + def _raise_defect(self, defect_level, message):
  821 + """
  822 + This method should be called for any defect found during file parsing.
  823 + It may raise an IOError exception according to the minimal level chosen
  824 + for the OleFileIO object.
  825 +
  826 + defect_level: defect level, possible values are:
  827 + DEFECT_UNSURE : a case which looks weird, but not sure it's a defect
  828 + DEFECT_POTENTIAL : a potential defect
  829 + DEFECT_INCORRECT : an error according to specifications, but parsing can go on
  830 + DEFECT_FATAL : an error which cannot be ignored, parsing is impossible
  831 + message: string describing the defect, used with raised exception.
  832 + """
  833 + # added by [PL]
  834 + if defect_level >= self._raise_defects_level:
  835 + raise IOError, message
  836 +
  837 +
  838 + def open(self, filename):
  839 + """
  840 + Open an OLE2 file.
  841 + Reads the header, FAT and directory.
  842 +
  843 + filename: string-like or file-like object
  844 + """
  845 + #[PL] check if filename is a string-like or file-like object:
  846 + # (it is better to check for a read() method)
  847 + if hasattr(filename, 'read'):
  848 + # file-like object
  849 + self.fp = filename
  850 + else:
  851 + # string-like object: filename of file on disk
  852 + #TODO: if larger than 1024 bytes, this could be the actual data => StringIO
  853 + self.fp = open(filename, "rb")
  854 + # old code fails if filename is not a plain string:
  855 + #if type(filename) == type(""):
  856 + # self.fp = open(filename, "rb")
  857 + #else:
  858 + # self.fp = filename
  859 + # obtain the filesize by using seek and tell, which should work on most
  860 + # file-like objects:
  861 + #TODO: do it above, using getsize with filename when possible?
  862 + #TODO: fix code to fail with clear exception when filesize cannot be obtained
  863 + self.fp.seek(0, os.SEEK_END)
  864 + try:
  865 + filesize = self.fp.tell()
  866 + finally:
  867 + self.fp.seek(0)
  868 + self._filesize = filesize
  869 +
  870 + # lists of streams in FAT and MiniFAT, to detect duplicate references
  871 + # (list of indexes of first sectors of each stream)
  872 + self._used_streams_fat = []
  873 + self._used_streams_minifat = []
  874 +
  875 + header = self.fp.read(512)
  876 +
  877 + if len(header) != 512 or header[:8] != MAGIC:
  878 + self._raise_defect(DEFECT_FATAL, "not an OLE2 structured storage file")
  879 +
  880 + # [PL] header structure according to AAF specifications:
  881 + ##Header
  882 + ##struct StructuredStorageHeader { // [offset from start (bytes), length (bytes)]
  883 + ##BYTE _abSig[8]; // [00H,08] {0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1,
  884 + ## // 0x1a, 0xe1} for current version
  885 + ##CLSID _clsid; // [08H,16] reserved must be zero (WriteClassStg/
  886 + ## // GetClassFile uses root directory class id)
  887 + ##USHORT _uMinorVersion; // [18H,02] minor version of the format: 33 is
  888 + ## // written by reference implementation
  889 + ##USHORT _uDllVersion; // [1AH,02] major version of the dll/format: 3 for
  890 + ## // 512-byte sectors, 4 for 4 KB sectors
  891 + ##USHORT _uByteOrder; // [1CH,02] 0xFFFE: indicates Intel byte-ordering
  892 + ##USHORT _uSectorShift; // [1EH,02] size of sectors in power-of-two;
  893 + ## // typically 9 indicating 512-byte sectors
  894 + ##USHORT _uMiniSectorShift; // [20H,02] size of mini-sectors in power-of-two;
  895 + ## // typically 6 indicating 64-byte mini-sectors
  896 + ##USHORT _usReserved; // [22H,02] reserved, must be zero
  897 + ##ULONG _ulReserved1; // [24H,04] reserved, must be zero
  898 + ##FSINDEX _csectDir; // [28H,04] must be zero for 512-byte sectors,
  899 + ## // number of SECTs in directory chain for 4 KB
  900 + ## // sectors
  901 + ##FSINDEX _csectFat; // [2CH,04] number of SECTs in the FAT chain
  902 + ##SECT _sectDirStart; // [30H,04] first SECT in the directory chain
  903 + ##DFSIGNATURE _signature; // [34H,04] signature used for transactions; must
  904 + ## // be zero. The reference implementation
  905 + ## // does not support transactions
  906 + ##ULONG _ulMiniSectorCutoff; // [38H,04] maximum size for a mini stream;
  907 + ## // typically 4096 bytes
  908 + ##SECT _sectMiniFatStart; // [3CH,04] first SECT in the MiniFAT chain
  909 + ##FSINDEX _csectMiniFat; // [40H,04] number of SECTs in the MiniFAT chain
  910 + ##SECT _sectDifStart; // [44H,04] first SECT in the DIFAT chain
  911 + ##FSINDEX _csectDif; // [48H,04] number of SECTs in the DIFAT chain
  912 + ##SECT _sectFat[109]; // [4CH,436] the SECTs of first 109 FAT sectors
  913 + ##};
  914 +
  915 + # [PL] header decoding:
  916 + # '<' indicates little-endian byte ordering for Intel (cf. struct module help)
  917 + fmt_header = '<8s16sHHHHHHLLLLLLLLLL'
  918 + header_size = struct.calcsize(fmt_header)
  919 + debug( "fmt_header size = %d, +FAT = %d" % (header_size, header_size + 109*4) )
  920 + header1 = header[:header_size]
  921 + (
  922 + self.Sig,
  923 + self.clsid,
  924 + self.MinorVersion,
  925 + self.DllVersion,
  926 + self.ByteOrder,
  927 + self.SectorShift,
  928 + self.MiniSectorShift,
  929 + self.Reserved, self.Reserved1,
  930 + self.csectDir,
  931 + self.csectFat,
  932 + self.sectDirStart,
  933 + self.signature,
  934 + self.MiniSectorCutoff,
  935 + self.MiniFatStart,
  936 + self.csectMiniFat,
  937 + self.sectDifStart,
  938 + self.csectDif
  939 + ) = struct.unpack(fmt_header, header1)
  940 + debug( struct.unpack(fmt_header, header1))
  941 +
  942 + if self.Sig != '\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1':
  943 + # OLE signature should always be present
  944 + self._raise_defect(DEFECT_FATAL, "incorrect OLE signature")
  945 + if self.clsid != '\x00'*16:
  946 + # according to AAF specs, CLSID should always be zero
  947 + self._raise_defect(DEFECT_INCORRECT, "incorrect CLSID in OLE header")
  948 + debug( "MinorVersion = %d" % self.MinorVersion )
  949 + debug( "DllVersion = %d" % self.DllVersion )
  950 + if self.DllVersion not in [3, 4]:
  951 + # version 3: usual format, 512 bytes per sector
  952 + # version 4: large format, 4K per sector
  953 + self._raise_defect(DEFECT_INCORRECT, "incorrect DllVersion in OLE header")
  954 + debug( "ByteOrder = %X" % self.ByteOrder )
  955 + if self.ByteOrder != 0xFFFE:
  956 + # For now only common little-endian documents are handled correctly
  957 + self._raise_defect(DEFECT_FATAL, "incorrect ByteOrder in OLE header")
  958 + # TODO: add big-endian support for documents created on Mac ?
  959 + self.SectorSize = 2**self.SectorShift
  960 + debug( "SectorSize = %d" % self.SectorSize )
  961 + if self.SectorSize not in [512, 4096]:
  962 + self._raise_defect(DEFECT_INCORRECT, "incorrect SectorSize in OLE header")
  963 + if (self.DllVersion==3 and self.SectorSize!=512) \
  964 + or (self.DllVersion==4 and self.SectorSize!=4096):
  965 + self._raise_defect(DEFECT_INCORRECT, "SectorSize does not match DllVersion in OLE header")
  966 + self.MiniSectorSize = 2**self.MiniSectorShift
  967 + debug( "MiniSectorSize = %d" % self.MiniSectorSize )
  968 + if self.MiniSectorSize not in [64]:
  969 + self._raise_defect(DEFECT_INCORRECT, "incorrect MiniSectorSize in OLE header")
  970 + if self.Reserved != 0 or self.Reserved1 != 0:
  971 + self._raise_defect(DEFECT_INCORRECT, "incorrect OLE header (non-null reserved bytes)")
  972 + debug( "csectDir = %d" % self.csectDir )
  973 + if self.SectorSize==512 and self.csectDir!=0:
  974 + self._raise_defect(DEFECT_INCORRECT, "incorrect csectDir in OLE header")
  975 + debug( "csectFat = %d" % self.csectFat )
  976 + debug( "sectDirStart = %X" % self.sectDirStart )
  977 + debug( "signature = %d" % self.signature )
  978 + # Signature should be zero, BUT some implementations do not follow this
  979 + # rule => only a potential defect:
  980 + if self.signature != 0:
  981 + self._raise_defect(DEFECT_POTENTIAL, "incorrect OLE header (signature>0)")
  982 + debug( "MiniSectorCutoff = %d" % self.MiniSectorCutoff )
  983 + debug( "MiniFatStart = %X" % self.MiniFatStart )
  984 + debug( "csectMiniFat = %d" % self.csectMiniFat )
  985 + debug( "sectDifStart = %X" % self.sectDifStart )
  986 + debug( "csectDif = %d" % self.csectDif )
  987 +
  988 + # calculate the number of sectors in the file
  989 + # (-1 because header doesn't count)
  990 + self.nb_sect = ( (filesize + self.SectorSize-1) / self.SectorSize) - 1
  991 + debug( "Number of sectors in the file: %d" % self.nb_sect )
  992 +
  993 + # file clsid (probably never used, so we don't store it)
  994 + clsid = _clsid(header[8:24])
  995 + self.sectorsize = self.SectorSize #1 << i16(header, 30)
  996 + self.minisectorsize = self.MiniSectorSize #1 << i16(header, 32)
  997 + self.minisectorcutoff = self.MiniSectorCutoff # i32(header, 56)
  998 +
  999 + # check known streams for duplicate references (these are always in FAT,
  1000 + # never in MiniFAT):
  1001 + self._check_duplicate_stream(self.sectDirStart)
  1002 + # check MiniFAT only if it is not empty:
  1003 + if self.csectMiniFat:
  1004 + self._check_duplicate_stream(self.MiniFatStart)
  1005 + # check DIFAT only if it is not empty:
  1006 + if self.csectDif:
  1007 + self._check_duplicate_stream(self.sectDifStart)
  1008 +
  1009 + # Load file allocation tables
  1010 + self.loadfat(header)
  1011 + # Load direcory. This sets both the direntries list (ordered by sid)
  1012 + # and the root (ordered by hierarchy) members.
  1013 + self.loaddirectory(self.sectDirStart)#i32(header, 48))
  1014 + self.ministream = None
  1015 + self.minifatsect = self.MiniFatStart #i32(header, 60)
  1016 +
  1017 +
  1018 + def close(self):
  1019 + """
  1020 + close the OLE file, to release the file object
  1021 + """
  1022 + self.fp.close()
  1023 +
  1024 +
  1025 + def _check_duplicate_stream(self, first_sect, minifat=False):
  1026 + """
  1027 + Checks if a stream has not been already referenced elsewhere.
  1028 + This method should only be called once for each known stream, and only
  1029 + if stream size is not null.
  1030 + first_sect: index of first sector of the stream in FAT
  1031 + minifat: if True, stream is located in the MiniFAT, else in the FAT
  1032 + """
  1033 + if minifat:
  1034 + debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect)
  1035 + used_streams = self._used_streams_minifat
  1036 + else:
  1037 + debug('_check_duplicate_stream: sect=%d in FAT' % first_sect)
  1038 + # some values can be safely ignored (not a real stream):
  1039 + if first_sect in (DIFSECT,FATSECT,ENDOFCHAIN,FREESECT):
  1040 + return
  1041 + used_streams = self._used_streams_fat
  1042 + #TODO: would it be more efficient using a dict or hash values, instead
  1043 + # of a list of long ?
  1044 + if first_sect in used_streams:
  1045 + self._raise_defect(DEFECT_INCORRECT, 'Stream referenced twice')
  1046 + else:
  1047 + used_streams.append(first_sect)
  1048 +
  1049 +
  1050 + def dumpfat(self, fat, firstindex=0):
  1051 + "Displays a part of FAT in human-readable form for debugging purpose"
  1052 + # [PL] added only for debug
  1053 + if not DEBUG_MODE:
  1054 + return
  1055 + # dictionary to convert special FAT values in human-readable strings
  1056 + VPL=8 # valeurs par ligne (8+1 * 8+1 = 81)
  1057 + fatnames = {
  1058 + FREESECT: "..free..",
  1059 + ENDOFCHAIN: "[ END. ]",
  1060 + FATSECT: "FATSECT ",
  1061 + DIFSECT: "DIFSECT "
  1062 + }
  1063 + nbsect = len(fat)
  1064 + nlines = (nbsect+VPL-1)/VPL
  1065 + print "index",
  1066 + for i in range(VPL):
  1067 + print ("%8X" % i),
  1068 + print ""
  1069 + for l in range(nlines):
  1070 + index = l*VPL
  1071 + print ("%8X:" % (firstindex+index)),
  1072 + for i in range(index, index+VPL):
  1073 + if i>=nbsect:
  1074 + break
  1075 + sect = fat[i]
  1076 + if sect in fatnames:
  1077 + nom = fatnames[sect]
  1078 + else:
  1079 + if sect == i+1:
  1080 + nom = " --->"
  1081 + else:
  1082 + nom = "%8X" % sect
  1083 + print nom,
  1084 + print ""
  1085 +
  1086 +
  1087 + def dumpsect(self, sector, firstindex=0):
  1088 + "Displays a sector in a human-readable form, for debugging purpose."
  1089 + if not DEBUG_MODE:
  1090 + return
  1091 + VPL=8 # number of values per line (8+1 * 8+1 = 81)
  1092 + tab = array.array(UINT32, sector)
  1093 + nbsect = len(tab)
  1094 + nlines = (nbsect+VPL-1)/VPL
  1095 + print "index",
  1096 + for i in range(VPL):
  1097 + print ("%8X" % i),
  1098 + print ""
  1099 + for l in range(nlines):
  1100 + index = l*VPL
  1101 + print ("%8X:" % (firstindex+index)),
  1102 + for i in range(index, index+VPL):
  1103 + if i>=nbsect:
  1104 + break
  1105 + sect = tab[i]
  1106 + nom = "%8X" % sect
  1107 + print nom,
  1108 + print ""
  1109 +
  1110 + def sect2array(self, sect):
  1111 + """
  1112 + convert a sector to an array of 32 bits unsigned integers,
  1113 + swapping bytes on big endian CPUs such as PowerPC (old Macs)
  1114 + """
  1115 + a = array.array(UINT32, sect)
  1116 + # if CPU is big endian, swap bytes:
  1117 + if sys.byteorder == 'big':
  1118 + a.byteswap()
  1119 + return a
  1120 +
  1121 +
  1122 + def loadfat_sect(self, sect):
  1123 + """
  1124 + Adds the indexes of the given sector to the FAT
  1125 + sect: string containing the first FAT sector, or array of long integers
  1126 + return: index of last FAT sector.
  1127 + """
  1128 + # a FAT sector is an array of ulong integers.
  1129 + if isinstance(sect, array.array):
  1130 + # if sect is already an array it is directly used
  1131 + fat1 = sect
  1132 + else:
  1133 + # if it's a raw sector, it is parsed in an array
  1134 + fat1 = self.sect2array(sect)
  1135 + self.dumpsect(sect)
  1136 + # The FAT is a sector chain starting at the first index of itself.
  1137 + for isect in fat1:
  1138 + #print "isect = %X" % isect
  1139 + if isect == ENDOFCHAIN or isect == FREESECT:
  1140 + # the end of the sector chain has been reached
  1141 + break
  1142 + # read the FAT sector
  1143 + s = self.getsect(isect)
  1144 + # parse it as an array of 32 bits integers, and add it to the
  1145 + # global FAT array
  1146 + nextfat = self.sect2array(s)
  1147 + self.fat = self.fat + nextfat
  1148 + return isect
  1149 +
  1150 +
  1151 + def loadfat(self, header):
  1152 + """
  1153 + Load the FAT table.
  1154 + """
  1155 + # The header contains a sector numbers
  1156 + # for the first 109 FAT sectors. Additional sectors are
  1157 + # described by DIF blocks
  1158 +
  1159 + sect = header[76:512]
  1160 + debug( "len(sect)=%d, so %d integers" % (len(sect), len(sect)/4) )
  1161 + #fat = []
  1162 + # [PL] FAT is an array of 32 bits unsigned ints, it's more effective
  1163 + # to use an array than a list in Python.
  1164 + # It's initialized as empty first:
  1165 + self.fat = array.array(UINT32)
  1166 + self.loadfat_sect(sect)
  1167 + #self.dumpfat(self.fat)
  1168 +## for i in range(0, len(sect), 4):
  1169 +## ix = i32(sect, i)
  1170 +## #[PL] if ix == -2 or ix == -1: # ix == 0xFFFFFFFEL or ix == 0xFFFFFFFFL:
  1171 +## if ix == 0xFFFFFFFEL or ix == 0xFFFFFFFFL:
  1172 +## break
  1173 +## s = self.getsect(ix)
  1174 +## #fat = fat + map(lambda i, s=s: i32(s, i), range(0, len(s), 4))
  1175 +## fat = fat + array.array(UINT32, s)
  1176 + if self.csectDif != 0:
  1177 + # [PL] There's a DIFAT because file is larger than 6.8MB
  1178 + # some checks just in case:
  1179 + if self.csectFat <= 109:
  1180 + # there must be at least 109 blocks in header and the rest in
  1181 + # DIFAT, so number of sectors must be >109.
  1182 + self._raise_defect(DEFECT_INCORRECT, 'incorrect DIFAT, not enough sectors')
  1183 + if self.sectDifStart >= self.nb_sect:
  1184 + # initial DIFAT block index must be valid
  1185 + self._raise_defect(DEFECT_FATAL, 'incorrect DIFAT, first index out of range')
  1186 + debug( "DIFAT analysis..." )
  1187 + # We compute the necessary number of DIFAT sectors :
  1188 + # (each DIFAT sector = 127 pointers + 1 towards next DIFAT sector)
  1189 + nb_difat = (self.csectFat-109 + 126)/127
  1190 + debug( "nb_difat = %d" % nb_difat )
  1191 + if self.csectDif != nb_difat:
  1192 + raise IOError, 'incorrect DIFAT'
  1193 + isect_difat = self.sectDifStart
  1194 + for i in xrange(nb_difat):
  1195 + debug( "DIFAT block %d, sector %X" % (i, isect_difat) )
  1196 + #TODO: check if corresponding FAT SID = DIFSECT
  1197 + sector_difat = self.getsect(isect_difat)
  1198 + difat = self.sect2array(sector_difat)
  1199 + self.dumpsect(sector_difat)
  1200 + self.loadfat_sect(difat[:127])
  1201 + # last DIFAT pointer is next DIFAT sector:
  1202 + isect_difat = difat[127]
  1203 + debug( "next DIFAT sector: %X" % isect_difat )
  1204 + # checks:
  1205 + if isect_difat not in [ENDOFCHAIN, FREESECT]:
  1206 + # last DIFAT pointer value must be ENDOFCHAIN or FREESECT
  1207 + raise IOError, 'incorrect end of DIFAT'
  1208 +## if len(self.fat) != self.csectFat:
  1209 +## # FAT should contain csectFat blocks
  1210 +## print "FAT length: %d instead of %d" % (len(self.fat), self.csectFat)
  1211 +## raise IOError, 'incorrect DIFAT'
  1212 + # since FAT is read from fixed-size sectors, it may contain more values
  1213 + # than the actual number of sectors in the file.
  1214 + # Keep only the relevant sector indexes:
  1215 + if len(self.fat) > self.nb_sect:
  1216 + debug('len(fat)=%d, shrunk to nb_sect=%d' % (len(self.fat), self.nb_sect))
  1217 + self.fat = self.fat[:self.nb_sect]
  1218 + debug('\nFAT:')
  1219 + self.dumpfat(self.fat)
  1220 +
  1221 +
  1222 + def loadminifat(self):
  1223 + """
  1224 + Load the MiniFAT table.
  1225 + """
  1226 + # MiniFAT is stored in a standard sub-stream, pointed to by a header
  1227 + # field.
  1228 + # NOTE: there are two sizes to take into account for this stream:
  1229 + # 1) Stream size is calculated according to the number of sectors
  1230 + # declared in the OLE header. This allocated stream may be more than
  1231 + # needed to store the actual sector indexes.
  1232 + # (self.csectMiniFat is the number of sectors of size self.SectorSize)
  1233 + stream_size = self.csectMiniFat * self.SectorSize
  1234 + # 2) Actually used size is calculated by dividing the MiniStream size
  1235 + # (given by root entry size) by the size of mini sectors, *4 for
  1236 + # 32 bits indexes:
  1237 + nb_minisectors = (self.root.size + self.MiniSectorSize-1) / self.MiniSectorSize
  1238 + used_size = nb_minisectors * 4
  1239 + debug('loadminifat(): minifatsect=%d, nb FAT sectors=%d, used_size=%d, stream_size=%d, nb MiniSectors=%d' %
  1240 + (self.minifatsect, self.csectMiniFat, used_size, stream_size, nb_minisectors))
  1241 + if used_size > stream_size:
  1242 + # This is not really a problem, but may indicate a wrong implementation:
  1243 + self._raise_defect(DEFECT_INCORRECT, 'OLE MiniStream is larger than MiniFAT')
  1244 + # In any case, first read stream_size:
  1245 + s = self._open(self.minifatsect, stream_size, force_FAT=True).read()
  1246 + #[PL] Old code replaced by an array:
  1247 + #self.minifat = map(lambda i, s=s: i32(s, i), range(0, len(s), 4))
  1248 + self.minifat = self.sect2array(s)
  1249 + # Then shrink the array to used size, to avoid indexes out of MiniStream:
  1250 + debug('MiniFAT shrunk from %d to %d sectors' % (len(self.minifat), nb_minisectors))
  1251 + self.minifat = self.minifat[:nb_minisectors]
  1252 + debug('loadminifat(): len=%d' % len(self.minifat))
  1253 + debug('\nMiniFAT:')
  1254 + self.dumpfat(self.minifat)
  1255 +
  1256 + def getsect(self, sect):
  1257 + """
  1258 + Read given sector from file on disk.
  1259 + sect: sector index
  1260 + returns a string containing the sector data.
  1261 + """
  1262 + # [PL] this original code was wrong when sectors are 4KB instead of
  1263 + # 512 bytes:
  1264 + #self.fp.seek(512 + self.sectorsize * sect)
  1265 + #[PL]: added safety checks:
  1266 + #print "getsect(%X)" % sect
  1267 + try:
  1268 + self.fp.seek(self.sectorsize * (sect+1))
  1269 + except:
  1270 + debug('getsect(): sect=%X, seek=%d, filesize=%d' %
  1271 + (sect, self.sectorsize*(sect+1), self._filesize))
  1272 + self._raise_defect(DEFECT_FATAL, 'OLE sector index out of range')
  1273 + sector = self.fp.read(self.sectorsize)
  1274 + if len(sector) != self.sectorsize:
  1275 + debug('getsect(): sect=%X, read=%d, sectorsize=%d' %
  1276 + (sect, len(sector), self.sectorsize))
  1277 + self._raise_defect(DEFECT_FATAL, 'incomplete OLE sector')
  1278 + return sector
  1279 +
  1280 +
  1281 + def loaddirectory(self, sect):
  1282 + """
  1283 + Load the directory.
  1284 + sect: sector index of directory stream.
  1285 + """
  1286 + # The directory is stored in a standard
  1287 + # substream, independent of its size.
  1288 +
  1289 + # open directory stream as a read-only file:
  1290 + # (stream size is not known in advance)
  1291 + self.directory_fp = self._open(sect)
  1292 +
  1293 + #[PL] to detect malformed documents and avoid DoS attacks, the maximum
  1294 + # number of directory entries can be calculated:
  1295 + max_entries = self.directory_fp.size / 128
  1296 + debug('loaddirectory: size=%d, max_entries=%d' %
  1297 + (self.directory_fp.size, max_entries))
  1298 +
  1299 + # Create list of directory entries
  1300 + #self.direntries = []
  1301 + # We start with a list of "None" object
  1302 + self.direntries = [None] * max_entries
  1303 +## for sid in xrange(max_entries):
  1304 +## entry = fp.read(128)
  1305 +## if not entry:
  1306 +## break
  1307 +## self.direntries.append(_OleDirectoryEntry(entry, sid, self))
  1308 + # load root entry:
  1309 + root_entry = self._load_direntry(0)
  1310 + # Root entry is the first entry:
  1311 + self.root = self.direntries[0]
  1312 + # read and build all storage trees, starting from the root:
  1313 + self.root.build_storage_tree()
  1314 +
  1315 +
  1316 + def _load_direntry (self, sid):
  1317 + """
  1318 + Load a directory entry from the directory.
  1319 + This method should only be called once for each storage/stream when
  1320 + loading the directory.
  1321 + sid: index of storage/stream in the directory.
  1322 + return: a _OleDirectoryEntry object
  1323 + raise: IOError if the entry has always been referenced.
  1324 + """
  1325 + # check if SID is OK:
  1326 + if sid<0 or sid>=len(self.direntries):
  1327 + self._raise_defect(DEFECT_FATAL, "OLE directory index out of range")
  1328 + # check if entry was already referenced:
  1329 + if self.direntries[sid] is not None:
  1330 + self._raise_defect(DEFECT_INCORRECT,
  1331 + "double reference for OLE stream/storage")
  1332 + # if exception not raised, return the object
  1333 + return self.direntries[sid]
  1334 + self.directory_fp.seek(sid * 128)
  1335 + entry = self.directory_fp.read(128)
  1336 + self.direntries[sid] = _OleDirectoryEntry(entry, sid, self)
  1337 + return self.direntries[sid]
  1338 +
  1339 +
  1340 + def dumpdirectory(self):
  1341 + """
  1342 + Dump directory (for debugging only)
  1343 + """
  1344 + self.root.dump()
  1345 +
  1346 +
  1347 + def _open(self, start, size = 0x7FFFFFFF, force_FAT=False):
  1348 + """
  1349 + Open a stream, either in FAT or MiniFAT according to its size.
  1350 + (openstream helper)
  1351 +
  1352 + start: index of first sector
  1353 + size: size of stream (or nothing if size is unknown)
  1354 + force_FAT: if False (default), stream will be opened in FAT or MiniFAT
  1355 + according to size. If True, it will always be opened in FAT.
  1356 + """
  1357 + debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' %
  1358 + (start, size, str(force_FAT)))
  1359 + # stream size is compared to the MiniSectorCutoff threshold:
  1360 + if size < self.minisectorcutoff and not force_FAT:
  1361 + # ministream object
  1362 + if not self.ministream:
  1363 + # load MiniFAT if it wasn't already done:
  1364 + self.loadminifat()
  1365 + # The first sector index of the miniFAT stream is stored in the
  1366 + # root directory entry:
  1367 + size_ministream = self.root.size
  1368 + debug('Opening MiniStream: sect=%d, size=%d' %
  1369 + (self.root.isectStart, size_ministream))
  1370 + self.ministream = self._open(self.root.isectStart,
  1371 + size_ministream, force_FAT=True)
  1372 + return _OleStream(self.ministream, start, size, 0,
  1373 + self.minisectorsize, self.minifat,
  1374 + self.ministream.size)
  1375 + else:
  1376 + # standard stream
  1377 + return _OleStream(self.fp, start, size, 512,
  1378 + self.sectorsize, self.fat, self._filesize)
  1379 +
  1380 +
  1381 + def _list(self, files, prefix, node):
  1382 + """
  1383 + (listdir helper)
  1384 + files: list of files to fill in
  1385 + prefix: current location in storage tree (list of names)
  1386 + node: current node (_OleDirectoryEntry object)
  1387 + """
  1388 + prefix = prefix + [node.name]
  1389 + for entry in node.kids:
  1390 + if entry.kids:
  1391 + self._list(files, prefix, entry)
  1392 + else:
  1393 + files.append(prefix[1:] + [entry.name])
  1394 +
  1395 +
  1396 + def listdir(self):
  1397 + """
  1398 + Return a list of streams stored in this file
  1399 + """
  1400 + files = []
  1401 + self._list(files, [], self.root)
  1402 + return files
  1403 +
  1404 +
  1405 + def _find(self, filename):
  1406 + """
  1407 + Returns directory entry of given filename. (openstream helper)
  1408 + Note: this method is case-insensitive.
  1409 +
  1410 + filename: path of stream in storage tree (except root entry), either:
  1411 + - a string using Unix path syntax, for example:
  1412 + 'storage_1/storage_1.2/stream'
  1413 + - a list of storage filenames, path to the desired stream/storage.
  1414 + Example: ['storage_1', 'storage_1.2', 'stream']
  1415 + return: sid of requested filename
  1416 + raise IOError if file not found
  1417 + """
  1418 +
  1419 + # if filename is a string instead of a list, split it on slashes to
  1420 + # convert to a list:
  1421 + if isinstance(filename, basestring):
  1422 + filename = filename.split('/')
  1423 + # walk across storage tree, following given path:
  1424 + node = self.root
  1425 + for name in filename:
  1426 + for kid in node.kids:
  1427 + if kid.name.lower() == name.lower():
  1428 + break
  1429 + else:
  1430 + raise IOError, "file not found"
  1431 + node = kid
  1432 + return node.sid
  1433 +
  1434 +
  1435 + def openstream(self, filename):
  1436 + """
  1437 + Open a stream as a read-only file object (StringIO).
  1438 +
  1439 + filename: path of stream in storage tree (except root entry), either:
  1440 + - a string using Unix path syntax, for example:
  1441 + 'storage_1/storage_1.2/stream'
  1442 + - a list of storage filenames, path to the desired stream/storage.
  1443 + Example: ['storage_1', 'storage_1.2', 'stream']
  1444 + return: file object (read-only)
  1445 + raise IOError if filename not found, or if this is not a stream.
  1446 + """
  1447 + sid = self._find(filename)
  1448 + entry = self.direntries[sid]
  1449 + if entry.entry_type != STGTY_STREAM:
  1450 + raise IOError, "this file is not a stream"
  1451 + return self._open(entry.isectStart, entry.size)
  1452 +
  1453 +
  1454 + def get_type(self, filename):
  1455 + """
  1456 + Test if given filename exists as a stream or a storage in the OLE
  1457 + container, and return its type.
  1458 +
  1459 + filename: path of stream in storage tree. (see openstream for syntax)
  1460 + return: False if object does not exist, its entry type (>0) otherwise:
  1461 + - STGTY_STREAM: a stream
  1462 + - STGTY_STORAGE: a storage
  1463 + - STGTY_ROOT: the root entry
  1464 + """
  1465 + try:
  1466 + sid = self._find(filename)
  1467 + entry = self.direntries[sid]
  1468 + return entry.entry_type
  1469 + except:
  1470 + return False
  1471 +
  1472 +
  1473 + def exists(self, filename):
  1474 + """
  1475 + Test if given filename exists as a stream or a storage in the OLE
  1476 + container.
  1477 +
  1478 + filename: path of stream in storage tree. (see openstream for syntax)
  1479 + return: True if object exist, else False.
  1480 + """
  1481 + try:
  1482 + sid = self._find(filename)
  1483 + return True
  1484 + except:
  1485 + return False
  1486 +
  1487 +
  1488 + def get_size(self, filename):
  1489 + """
  1490 + Return size of a stream in the OLE container, in bytes.
  1491 +
  1492 + filename: path of stream in storage tree (see openstream for syntax)
  1493 + return: size in bytes (long integer)
  1494 + raise: IOError if file not found, TypeError if this is not a stream.
  1495 + """
  1496 + sid = self._find(filename)
  1497 + entry = self.direntries[sid]
  1498 + if entry.entry_type != STGTY_STREAM:
  1499 + #TODO: Should it return zero instead of raising an exception ?
  1500 + raise TypeError, 'object is not an OLE stream'
  1501 + return entry.size
  1502 +
  1503 +
  1504 + def get_rootentry_name(self):
  1505 + """
  1506 + Return root entry name. Should usually be 'Root Entry' or 'R' in most
  1507 + implementations.
  1508 + """
  1509 + return self.root.name
  1510 +
  1511 +
  1512 + def getproperties(self, filename):
  1513 + """
  1514 + Return properties described in substream.
  1515 +
  1516 + filename: path of stream in storage tree (see openstream for syntax)
  1517 + return: a dictionary of values indexed by id (integer)
  1518 + """
  1519 + fp = self.openstream(filename)
  1520 +
  1521 + data = {}
  1522 +
  1523 + # header
  1524 + s = fp.read(28)
  1525 + clsid = _clsid(s[8:24])
  1526 +
  1527 + # format id
  1528 + s = fp.read(20)
  1529 + fmtid = _clsid(s[:16])
  1530 + fp.seek(i32(s, 16))
  1531 +
  1532 + # get section
  1533 + s = "****" + fp.read(i32(fp.read(4))-4)
  1534 +
  1535 + for i in range(i32(s, 4)):
  1536 +
  1537 + id = i32(s, 8+i*8)
  1538 + offset = i32(s, 12+i*8)
  1539 + type = i32(s, offset)
  1540 +
  1541 + debug ('property id=%d: type=%d offset=%X' % (id, type, offset))
  1542 +
  1543 + # test for common types first (should perhaps use
  1544 + # a dictionary instead?)
  1545 +
  1546 + if type == VT_I2:
  1547 + value = i16(s, offset+4)
  1548 + if value >= 32768:
  1549 + value = value - 65536
  1550 + elif type == VT_UI2:
  1551 + value = i16(s, offset+4)
  1552 + elif type in (VT_I4, VT_ERROR):
  1553 + value = i32(s, offset+4)
  1554 + elif type == VT_UI4:
  1555 + value = i32(s, offset+4) # FIXME
  1556 + elif type in (VT_BSTR, VT_LPSTR):
  1557 + count = i32(s, offset+4)
  1558 + value = s[offset+8:offset+8+count-1]
  1559 + elif type == VT_BLOB:
  1560 + count = i32(s, offset+4)
  1561 + value = s[offset+8:offset+8+count]
  1562 + elif type == VT_LPWSTR:
  1563 + count = i32(s, offset+4)
  1564 + value = _unicode(s[offset+8:offset+8+count*2])
  1565 + elif type == VT_FILETIME:
  1566 + value = long(i32(s, offset+4)) + (long(i32(s, offset+8))<<32)
  1567 + # FIXME: this is a 64-bit int: "number of 100ns periods
  1568 + # since Jan 1,1601". Should map this to Python time
  1569 + value = value / 10000000L # seconds
  1570 + elif type == VT_UI1:
  1571 + value = ord(s[offset+4])
  1572 + elif type == VT_CLSID:
  1573 + value = _clsid(s[offset+4:offset+20])
  1574 + elif type == VT_CF:
  1575 + count = i32(s, offset+4)
  1576 + value = s[offset+8:offset+8+count]
  1577 + else:
  1578 + value = None # everything else yields "None"
  1579 +
  1580 + # FIXME: add support for VT_VECTOR
  1581 +
  1582 + #print "%08x" % id, repr(value),
  1583 + #print "(%s)" % VT[i32(s, offset) & 0xFFF]
  1584 +
  1585 + data[id] = value
  1586 +
  1587 + return data
  1588 +
  1589 +#
  1590 +# --------------------------------------------------------------------
  1591 +# This script can be used to dump the directory of any OLE2 structured
  1592 +# storage file.
  1593 +
  1594 +if __name__ == "__main__":
  1595 +
  1596 + import sys
  1597 +
  1598 + # [PL] display quick usage info if launched from command-line
  1599 + if len(sys.argv) <= 1:
  1600 + print __doc__
  1601 + print """
  1602 +Launched from command line, this script parses OLE files and prints info.
  1603 +
  1604 +Usage: OleFileIO_PL.py [-d] [-c] <file> [file2 ...]
  1605 +
  1606 +Options:
  1607 +-d : debug mode (display a lot of debug information, for developers only)
  1608 +-c : check all streams (for debugging purposes)
  1609 +"""
  1610 + sys.exit()
  1611 +
  1612 + check_streams = False
  1613 + for filename in sys.argv[1:]:
  1614 +## try:
  1615 + # OPTIONS:
  1616 + if filename == '-d':
  1617 + # option to switch debug mode on:
  1618 + set_debug_mode(True)
  1619 + continue
  1620 + if filename == '-c':
  1621 + # option to switch check streams mode on:
  1622 + check_streams = True
  1623 + continue
  1624 +
  1625 + ole = OleFileIO(filename, raise_defects=DEFECT_INCORRECT)
  1626 + print "-" * 68
  1627 + print filename
  1628 + print "-" * 68
  1629 + ole.dumpdirectory()
  1630 + for streamname in ole.listdir():
  1631 + if streamname[-1][0] == "\005":
  1632 + print streamname, ": properties"
  1633 + props = ole.getproperties(streamname)
  1634 + props = props.items()
  1635 + props.sort()
  1636 + for k, v in props:
  1637 + #[PL]: avoid to display too large or binary values:
  1638 + if isinstance(v, basestring):
  1639 + if len(v) > 50:
  1640 + v = v[:50]
  1641 + # quick and dirty binary check:
  1642 + for c in (1,2,3,4,5,6,7,11,12,14,15,16,17,18,19,20,
  1643 + 21,22,23,24,25,26,27,28,29,30,31):
  1644 + if chr(c) in v:
  1645 + v = '(binary data)'
  1646 + break
  1647 + print " ", k, v
  1648 +
  1649 + if check_streams:
  1650 + # Read all streams to check if there are errors:
  1651 + print '\nChecking streams...'
  1652 + for streamname in ole.listdir():
  1653 + # print name using repr() to convert binary chars to \xNN:
  1654 + print '-', repr('/'.join(streamname)),'-',
  1655 + st_type = ole.get_type(streamname)
  1656 + if st_type == STGTY_STREAM:
  1657 + print 'size %d' % ole.get_size(streamname)
  1658 + # just try to read stream in memory:
  1659 + ole.openstream(streamname)
  1660 + else:
  1661 + print 'NOT a stream : type=%d' % st_type
  1662 + print ''
  1663 +
  1664 + #[PL] Test a few new methods:
  1665 + root = ole.get_rootentry_name()
  1666 + print 'Root entry name: "%s"' % root
  1667 + if ole.exists('worddocument'):
  1668 + print "This is a Word document."
  1669 + print "type of stream 'WordDocument':", ole.get_type('worddocument')
  1670 + print "size :", ole.get_size('worddocument')
  1671 + if ole.exists('macros/vba'):
  1672 + print "This document may contain VBA macros."
  1673 +## except IOError, v:
  1674 +## print "***", "cannot read", file, "-", v
... ...
oletools/thirdparty/OleFileIO_PL/README.txt 0 → 100644
  1 +++ a/oletools/thirdparty/OleFileIO_PL/README.txt
  1 +OleFileIO\_PL
  2 +=============
  3 +
  4 +`OleFileIO\_PL <http://www.decalage.info/python/olefileio>`_ is a Python
  5 +module to read `Microsoft OLE2 files (also called Structured Storage,
  6 +Compound File Binary Format or Compound Document File
  7 +Format) <http://en.wikipedia.org/wiki/Compound_File_Binary_Format>`_,
  8 +such as Microsoft Office documents, Image Composer and FlashPix files,
  9 +Outlook messages, ...
  10 +
  11 +This is an improved version of the OleFileIO module from
  12 +`PIL <http://www.pythonware.com/products/pil/index.htm>`_, the excellent
  13 +Python Imaging Library, created and maintained by Fredrik Lundh. The API
  14 +is still compatible with PIL, but I have improved the internal
  15 +implementation significantly, with bugfixes and a more robust design.
  16 +
  17 +As far as I know, this module is now the most complete and robust Python
  18 +implementation to read MS OLE2 files, portable on several operating
  19 +systems. (please tell me if you know other similar Python modules)
  20 +
  21 +WARNING: THIS IS (STILL) WORK IN PROGRESS.
  22 +
  23 +Main improvements over PIL version:
  24 +-----------------------------------
  25 +
  26 +- Better compatibility with Python 2.4 up to 2.7
  27 +- Support for files larger than 6.8MB
  28 +- Robust: many checks to detect malformed files
  29 +- Improved API
  30 +- Added setup.py and install.bat to ease installation
  31 +
  32 +News
  33 +----
  34 +
  35 +- 2012-09-11 v0.23: added support for file-like objects, fixed `issue
  36 + #8 <https://bitbucket.org/decalage/olefileio_pl/issue/8/bug-with-file-object>`_
  37 +- 2012-02-17 v0.22: fixed issues #7 (bug in getproperties) and #2
  38 + (added close method)
  39 +- 2011-10-20: code hosted on bitbucket to ease contributions and bug
  40 + tracking
  41 +- 2010-01-24 v0.21: fixed support for big-endian CPUs, such as PowerPC
  42 + Macs.
  43 +- 2009-12-11 v0.20: small bugfix in OleFileIO.open when filename is not
  44 + plain str.
  45 +- 2009-12-10 v0.19: fixed support for 64 bits platforms (thanks to Ben
  46 + G. and Martijn for reporting the bug)
  47 +- see changelog in source code for more info.
  48 +
  49 +Download:
  50 +---------
  51 +
  52 +The archive is available on `the project
  53 +page <https://bitbucket.org/decalage/olefileio_pl/downloads>`_.
  54 +
  55 +How to use this module:
  56 +-----------------------
  57 +
  58 +See sample code at the end of the module, and also docstrings.
  59 +
  60 +Here are a few examples:
  61 +
  62 +::
  63 +
  64 + import OleFileIO_PL
  65 +
  66 + # Test if a file is an OLE container:
  67 + assert OleFileIO_PL.isOleFile('myfile.doc')
  68 +
  69 + # Open an OLE file from disk:
  70 + ole = OleFileIO_PL.OleFileIO('myfile.doc')
  71 +
  72 + # Get list of streams:
  73 + print ole.listdir()
  74 +
  75 + # Test if known streams/storages exist:
  76 + if ole.exists('worddocument'):
  77 + print "This is a Word document."
  78 + print "size :", ole.get_size('worddocument')
  79 + if ole.exists('macros/vba'):
  80 + print "This document seems to contain VBA macros."
  81 +
  82 + # Extract the "Pictures" stream from a PPT file:
  83 + if ole.exists('Pictures'):
  84 + pics = ole.openstream('Pictures')
  85 + data = pics.read()
  86 + f = open('Pictures.bin', 'w')
  87 + f.write(data)
  88 + f.close()
  89 +
  90 + # Close the OLE file:
  91 + ole.close()
  92 +
  93 + # Work with a file-like object (e.g. StringIO) instead of a file on disk:
  94 + data = open('myfile.doc', 'rb').read()
  95 + f = StringIO.StringIO(data)
  96 + ole = OleFileIO_PL.OleFileIO(f)
  97 + print ole.listdir()
  98 + ole.close()
  99 +
  100 +It can also be used as a script from the command-line to display the
  101 +structure of an OLE file, for example:
  102 +
  103 +::
  104 +
  105 + OleFileIO_PL.py myfile.doc
  106 +
  107 +A real-life example: `using OleFileIO\_PL for malware analysis and
  108 +forensics <http://blog.gregback.net/2011/03/using-remnux-for-forensic-puzzle-6/>`_.
  109 +
  110 +How to contribute:
  111 +------------------
  112 +
  113 +The code is available in `a Mercurial repository on
  114 +bitbucket <https://bitbucket.org/decalage/olefileio_pl>`_. You may use
  115 +it to submit enhancements or to report any issue.
  116 +
  117 +If you would like to help us improve this module, or simply provide
  118 +feedback, you may also send an e-mail to decalage(at)laposte.net. You
  119 +can help in many ways:
  120 +
  121 +- test this module on different platforms / Python versions
  122 +- find and report bugs
  123 +- improve documentation, code samples, docstrings
  124 +- write unittest test cases
  125 +- provide tricky malformed files
  126 +
  127 +How to report bugs:
  128 +-------------------
  129 +
  130 +To report a bug, for example a normal file which is not parsed
  131 +correctly, please use the `issue reporting
  132 +page <https://bitbucket.org/decalage/olefileio_pl/issues?status=new&status=open>`_,
  133 +or send an e-mail with an attachment containing the debugging output of
  134 +OleFileIO\_PL.
  135 +
  136 +For this, launch the following command :
  137 +
  138 +::
  139 +
  140 + OleFileIO_PL.py -d -c file >debug.txt
  141 +
  142 +License
  143 +-------
  144 +
  145 +OleFileIO\_PL is open-source.
  146 +
  147 +OleFileIO\_PL changes are Copyright (c) 2005-2012 by Philippe Lagadec.
  148 +
  149 +The Python Imaging Library (PIL) is
  150 +
  151 +- Copyright (c) 1997-2005 by Secret Labs AB
  152 +
  153 +- Copyright (c) 1995-2005 by Fredrik Lundh
  154 +
  155 +By obtaining, using, and/or copying this software and/or its associated
  156 +documentation, you agree that you have read, understood, and will comply
  157 +with the following terms and conditions:
  158 +
  159 +Permission to use, copy, modify, and distribute this software and its
  160 +associated documentation for any purpose and without fee is hereby
  161 +granted, provided that the above copyright notice appears in all copies,
  162 +and that both that copyright notice and this permission notice appear in
  163 +supporting documentation, and that the name of Secret Labs AB or the
  164 +author not be used in advertising or publicity pertaining to
  165 +distribution of the software without specific, written prior permission.
  166 +
  167 +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO
  168 +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
  169 +FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR
  170 +ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
  171 +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
  172 +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  173 +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
... ...
oletools/thirdparty/OleFileIO_PL/__init__.py 0 → 100644
  1 +++ a/oletools/thirdparty/OleFileIO_PL/__init__.py
... ...
oletools/thirdparty/__init__.py 0 → 100644
  1 +++ a/oletools/thirdparty/__init__.py
... ...
oletools/thirdparty/easygui/LICENSE.txt 0 → 100644
  1 +++ a/oletools/thirdparty/easygui/LICENSE.txt
  1 +LICENSE INFORMATION
  2 +
  3 +EasyGui version 0.96
  4 +
  5 +Copyright (c) 2010, Stephen Raymond Ferg
  6 +
  7 +All rights reserved.
  8 +
  9 +Redistribution and use in source and binary forms, with or without modification,
  10 +are permitted provided that the following conditions are met:
  11 +
  12 + 1. Redistributions of source code must retain the above copyright notice,
  13 + this list of conditions and the following disclaimer.
  14 +
  15 + 2. Redistributions in binary form must reproduce the above copyright notice,
  16 + this list of conditions and the following disclaimer in the documentation and/or
  17 + other materials provided with the distribution.
  18 +
  19 + 3. The name of the author may not be used to endorse or promote products derived
  20 + from this software without specific prior written permission.
  21 +
  22 +THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS"
  23 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  24 +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  25 +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
  26 +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  27 +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  28 +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  29 +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  30 +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
  31 +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
  32 +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
... ...
oletools/thirdparty/easygui/__init__.py 0 → 100644
  1 +++ a/oletools/thirdparty/easygui/__init__.py
... ...
oletools/thirdparty/easygui/easygui.py 0 → 100644
  1 +++ a/oletools/thirdparty/easygui/easygui.py
  1 +"""
  2 +@version: 0.96(2010-08-29)
  3 +
  4 +@note:
  5 +ABOUT EASYGUI
  6 +
  7 +EasyGui provides an easy-to-use interface for simple GUI interaction
  8 +with a user. It does not require the programmer to know anything about
  9 +tkinter, frames, widgets, callbacks or lambda. All GUI interactions are
  10 +invoked by simple function calls that return results.
  11 +
  12 +@note:
  13 +WARNING about using EasyGui with IDLE
  14 +
  15 +You may encounter problems using IDLE to run programs that use EasyGui. Try it
  16 +and find out. EasyGui is a collection of Tkinter routines that run their own
  17 +event loops. IDLE is also a Tkinter application, with its own event loop. The
  18 +two may conflict, with unpredictable results. If you find that you have
  19 +problems, try running your EasyGui program outside of IDLE.
  20 +
  21 +Note that EasyGui requires Tk release 8.0 or greater.
  22 +
  23 +@note:
  24 +LICENSE INFORMATION
  25 +
  26 +EasyGui version 0.96
  27 +
  28 +Copyright (c) 2010, Stephen Raymond Ferg
  29 +
  30 +All rights reserved.
  31 +
  32 +Redistribution and use in source and binary forms, with or without modification,
  33 +are permitted provided that the following conditions are met:
  34 +
  35 + 1. Redistributions of source code must retain the above copyright notice,
  36 + this list of conditions and the following disclaimer.
  37 +
  38 + 2. Redistributions in binary form must reproduce the above copyright notice,
  39 + this list of conditions and the following disclaimer in the documentation and/or
  40 + other materials provided with the distribution.
  41 +
  42 + 3. The name of the author may not be used to endorse or promote products derived
  43 + from this software without specific prior written permission.
  44 +
  45 +THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS"
  46 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  47 +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  48 +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
  49 +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  50 +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  51 +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  52 +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  53 +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
  54 +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
  55 +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  56 +
  57 +@note:
  58 +ABOUT THE EASYGUI LICENSE
  59 +
  60 +This license is what is generally known as the "modified BSD license",
  61 +aka "revised BSD", "new BSD", "3-clause BSD".
  62 +See http://www.opensource.org/licenses/bsd-license.php
  63 +
  64 +This license is GPL-compatible.
  65 +See http://en.wikipedia.org/wiki/License_compatibility
  66 +See http://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses
  67 +
  68 +The BSD License is less restrictive than GPL.
  69 +It allows software released under the license to be incorporated into proprietary products.
  70 +Works based on the software may be released under a proprietary license or as closed source software.
  71 +http://en.wikipedia.org/wiki/BSD_licenses#3-clause_license_.28.22New_BSD_License.22.29
  72 +
  73 +"""
  74 +egversion = __doc__.split()[1]
  75 +
  76 +__all__ = ['ynbox'
  77 + , 'ccbox'
  78 + , 'boolbox'
  79 + , 'indexbox'
  80 + , 'msgbox'
  81 + , 'buttonbox'
  82 + , 'integerbox'
  83 + , 'multenterbox'
  84 + , 'enterbox'
  85 + , 'exceptionbox'
  86 + , 'choicebox'
  87 + , 'codebox'
  88 + , 'textbox'
  89 + , 'diropenbox'
  90 + , 'fileopenbox'
  91 + , 'filesavebox'
  92 + , 'passwordbox'
  93 + , 'multpasswordbox'
  94 + , 'multchoicebox'
  95 + , 'abouteasygui'
  96 + , 'egversion'
  97 + , 'egdemo'
  98 + , 'EgStore'
  99 + ]
  100 +
  101 +import sys, os
  102 +import string
  103 +import pickle
  104 +import traceback
  105 +
  106 +
  107 +#--------------------------------------------------
  108 +# check python version and take appropriate action
  109 +#--------------------------------------------------
  110 +"""
  111 +From the python documentation:
  112 +
  113 +sys.hexversion contains the version number encoded as a single integer. This is
  114 +guaranteed to increase with each version, including proper support for non-
  115 +production releases. For example, to test that the Python interpreter is at
  116 +least version 1.5.2, use:
  117 +
  118 +if sys.hexversion >= 0x010502F0:
  119 + # use some advanced feature
  120 + ...
  121 +else:
  122 + # use an alternative implementation or warn the user
  123 + ...
  124 +"""
  125 +
  126 +
  127 +if sys.hexversion >= 0x020600F0:
  128 + runningPython26 = True
  129 +else:
  130 + runningPython26 = False
  131 +
  132 +if sys.hexversion >= 0x030000F0:
  133 + runningPython3 = True
  134 +else:
  135 + runningPython3 = False
  136 +
  137 +try:
  138 + from PIL import Image as PILImage
  139 + from PIL import ImageTk as PILImageTk
  140 + PILisLoaded = True
  141 +except:
  142 + PILisLoaded = False
  143 +
  144 +
  145 +if runningPython3:
  146 + from tkinter import *
  147 + import tkinter.filedialog as tk_FileDialog
  148 + from io import StringIO
  149 +else:
  150 + from Tkinter import *
  151 + import tkFileDialog as tk_FileDialog
  152 + from StringIO import StringIO
  153 +
  154 +def write(*args):
  155 + args = [str(arg) for arg in args]
  156 + args = " ".join(args)
  157 + sys.stdout.write(args)
  158 +
  159 +def writeln(*args):
  160 + write(*args)
  161 + sys.stdout.write("\n")
  162 +
  163 +say = writeln
  164 +
  165 +
  166 +if TkVersion < 8.0 :
  167 + stars = "*"*75
  168 + writeln("""\n\n\n""" + stars + """
  169 +You are running Tk version: """ + str(TkVersion) + """
  170 +You must be using Tk version 8.0 or greater to use EasyGui.
  171 +Terminating.
  172 +""" + stars + """\n\n\n""")
  173 + sys.exit(0)
  174 +
  175 +def dq(s):
  176 + return '"%s"' % s
  177 +
  178 +rootWindowPosition = "+300+200"
  179 +
  180 +PROPORTIONAL_FONT_FAMILY = ("MS", "Sans", "Serif")
  181 +MONOSPACE_FONT_FAMILY = ("Courier")
  182 +
  183 +PROPORTIONAL_FONT_SIZE = 10
  184 +MONOSPACE_FONT_SIZE = 9 #a little smaller, because it it more legible at a smaller size
  185 +TEXT_ENTRY_FONT_SIZE = 12 # a little larger makes it easier to see
  186 +
  187 +#STANDARD_SELECTION_EVENTS = ["Return", "Button-1"]
  188 +STANDARD_SELECTION_EVENTS = ["Return", "Button-1", "space"]
  189 +
  190 +# Initialize some global variables that will be reset later
  191 +__choiceboxMultipleSelect = None
  192 +__widgetTexts = None
  193 +__replyButtonText = None
  194 +__choiceboxResults = None
  195 +__firstWidget = None
  196 +__enterboxText = None
  197 +__enterboxDefaultText=""
  198 +__multenterboxText = ""
  199 +choiceboxChoices = None
  200 +choiceboxWidget = None
  201 +entryWidget = None
  202 +boxRoot = None
  203 +ImageErrorMsg = (
  204 + "\n\n---------------------------------------------\n"
  205 + "Error: %s\n%s")
  206 +#-------------------------------------------------------------------
  207 +# various boxes built on top of the basic buttonbox
  208 +#-----------------------------------------------------------------------
  209 +
  210 +#-----------------------------------------------------------------------
  211 +# ynbox
  212 +#-----------------------------------------------------------------------
  213 +def ynbox(msg="Shall I continue?"
  214 + , title=" "
  215 + , choices=("Yes", "No")
  216 + , image=None
  217 + ):
  218 + """
  219 + Display a msgbox with choices of Yes and No.
  220 +
  221 + The default is "Yes".
  222 +
  223 + The returned value is calculated this way::
  224 + if the first choice ("Yes") is chosen, or if the dialog is cancelled:
  225 + return 1
  226 + else:
  227 + return 0
  228 +
  229 + If invoked without a msg argument, displays a generic request for a confirmation
  230 + that the user wishes to continue. So it can be used this way::
  231 + if ynbox(): pass # continue
  232 + else: sys.exit(0) # exit the program
  233 +
  234 + @arg msg: the msg to be displayed.
  235 + @arg title: the window title
  236 + @arg choices: a list or tuple of the choices to be displayed
  237 + """
  238 + return boolbox(msg, title, choices, image=image)
  239 +
  240 +
  241 +#-----------------------------------------------------------------------
  242 +# ccbox
  243 +#-----------------------------------------------------------------------
  244 +def ccbox(msg="Shall I continue?"
  245 + , title=" "
  246 + , choices=("Continue", "Cancel")
  247 + , image=None
  248 + ):
  249 + """
  250 + Display a msgbox with choices of Continue and Cancel.
  251 +
  252 + The default is "Continue".
  253 +
  254 + The returned value is calculated this way::
  255 + if the first choice ("Continue") is chosen, or if the dialog is cancelled:
  256 + return 1
  257 + else:
  258 + return 0
  259 +
  260 + If invoked without a msg argument, displays a generic request for a confirmation
  261 + that the user wishes to continue. So it can be used this way::
  262 +
  263 + if ccbox():
  264 + pass # continue
  265 + else:
  266 + sys.exit(0) # exit the program
  267 +
  268 + @arg msg: the msg to be displayed.
  269 + @arg title: the window title
  270 + @arg choices: a list or tuple of the choices to be displayed
  271 + """
  272 + return boolbox(msg, title, choices, image=image)
  273 +
  274 +
  275 +#-----------------------------------------------------------------------
  276 +# boolbox
  277 +#-----------------------------------------------------------------------
  278 +def boolbox(msg="Shall I continue?"
  279 + , title=" "
  280 + , choices=("Yes","No")
  281 + , image=None
  282 + ):
  283 + """
  284 + Display a boolean msgbox.
  285 +
  286 + The default is the first choice.
  287 +
  288 + The returned value is calculated this way::
  289 + if the first choice is chosen, or if the dialog is cancelled:
  290 + returns 1
  291 + else:
  292 + returns 0
  293 + """
  294 + reply = buttonbox(msg=msg, choices=choices, title=title, image=image)
  295 + if reply == choices[0]: return 1
  296 + else: return 0
  297 +
  298 +
  299 +#-----------------------------------------------------------------------
  300 +# indexbox
  301 +#-----------------------------------------------------------------------
  302 +def indexbox(msg="Shall I continue?"
  303 + , title=" "
  304 + , choices=("Yes","No")
  305 + , image=None
  306 + ):
  307 + """
  308 + Display a buttonbox with the specified choices.
  309 + Return the index of the choice selected.
  310 + """
  311 + reply = buttonbox(msg=msg, choices=choices, title=title, image=image)
  312 + index = -1
  313 + for choice in choices:
  314 + index = index + 1
  315 + if reply == choice: return index
  316 + raise AssertionError(
  317 + "There is a program logic error in the EasyGui code for indexbox.")
  318 +
  319 +
  320 +#-----------------------------------------------------------------------
  321 +# msgbox
  322 +#-----------------------------------------------------------------------
  323 +def msgbox(msg="(Your message goes here)", title=" ", ok_button="OK",image=None,root=None):
  324 + """
  325 + Display a messagebox
  326 + """
  327 + if type(ok_button) != type("OK"):
  328 + raise AssertionError("The 'ok_button' argument to msgbox must be a string.")
  329 +
  330 + return buttonbox(msg=msg, title=title, choices=[ok_button], image=image,root=root)
  331 +
  332 +
  333 +#-------------------------------------------------------------------
  334 +# buttonbox
  335 +#-------------------------------------------------------------------
  336 +def buttonbox(msg="",title=" "
  337 + ,choices=("Button1", "Button2", "Button3")
  338 + , image=None
  339 + , root=None
  340 + ):
  341 + """
  342 + Display a msg, a title, and a set of buttons.
  343 + The buttons are defined by the members of the choices list.
  344 + Return the text of the button that the user selected.
  345 +
  346 + @arg msg: the msg to be displayed.
  347 + @arg title: the window title
  348 + @arg choices: a list or tuple of the choices to be displayed
  349 + """
  350 + global boxRoot, __replyButtonText, __widgetTexts, buttonsFrame
  351 +
  352 +
  353 + # Initialize __replyButtonText to the first choice.
  354 + # This is what will be used if the window is closed by the close button.
  355 + __replyButtonText = choices[0]
  356 +
  357 + if root:
  358 + root.withdraw()
  359 + boxRoot = Toplevel(master=root)
  360 + boxRoot.withdraw()
  361 + else:
  362 + boxRoot = Tk()
  363 + boxRoot.withdraw()
  364 +
  365 + boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose )
  366 + boxRoot.title(title)
  367 + boxRoot.iconname('Dialog')
  368 + boxRoot.geometry(rootWindowPosition)
  369 + boxRoot.minsize(400, 100)
  370 +
  371 + # ------------- define the messageFrame ---------------------------------
  372 + messageFrame = Frame(master=boxRoot)
  373 + messageFrame.pack(side=TOP, fill=BOTH)
  374 +
  375 + # ------------- define the imageFrame ---------------------------------
  376 + tk_Image = None
  377 + if image:
  378 + imageFilename = os.path.normpath(image)
  379 + junk,ext = os.path.splitext(imageFilename)
  380 +
  381 + if os.path.exists(imageFilename):
  382 + if ext.lower() in [".gif", ".pgm", ".ppm"]:
  383 + tk_Image = PhotoImage(master=boxRoot, file=imageFilename)
  384 + else:
  385 + if PILisLoaded:
  386 + try:
  387 + pil_Image = PILImage.open(imageFilename)
  388 + tk_Image = PILImageTk.PhotoImage(pil_Image, master=boxRoot)
  389 + except:
  390 + msg += ImageErrorMsg % (imageFilename,
  391 + "\nThe Python Imaging Library (PIL) could not convert this file to a displayable image."
  392 + "\n\nPIL reports:\n" + exception_format())
  393 +
  394 + else: # PIL is not loaded
  395 + msg += ImageErrorMsg % (imageFilename,
  396 + "\nI could not import the Python Imaging Library (PIL) to display the image.\n\n"
  397 + "You may need to install PIL\n"
  398 + "(http://www.pythonware.com/products/pil/)\n"
  399 + "to display " + ext + " image files.")
  400 +
  401 + else:
  402 + msg += ImageErrorMsg % (imageFilename, "\nImage file not found.")
  403 +
  404 + if tk_Image:
  405 + imageFrame = Frame(master=boxRoot)
  406 + imageFrame.pack(side=TOP, fill=BOTH)
  407 + label = Label(imageFrame,image=tk_Image)
  408 + label.image = tk_Image # keep a reference!
  409 + label.pack(side=TOP, expand=YES, fill=X, padx='1m', pady='1m')
  410 +
  411 + # ------------- define the buttonsFrame ---------------------------------
  412 + buttonsFrame = Frame(master=boxRoot)
  413 + buttonsFrame.pack(side=TOP, fill=BOTH)
  414 +
  415 + # -------------------- place the widgets in the frames -----------------------
  416 + messageWidget = Message(messageFrame, text=msg, width=400)
  417 + messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE))
  418 + messageWidget.pack(side=TOP, expand=YES, fill=X, padx='3m', pady='3m')
  419 +
  420 + __put_buttons_in_buttonframe(choices)
  421 +
  422 + # -------------- the action begins -----------
  423 + # put the focus on the first button
  424 + __firstWidget.focus_force()
  425 +
  426 + boxRoot.deiconify()
  427 + boxRoot.mainloop()
  428 + boxRoot.destroy()
  429 + if root: root.deiconify()
  430 + return __replyButtonText
  431 +
  432 +
  433 +#-------------------------------------------------------------------
  434 +# integerbox
  435 +#-------------------------------------------------------------------
  436 +def integerbox(msg=""
  437 + , title=" "
  438 + , default=""
  439 + , lowerbound=0
  440 + , upperbound=99
  441 + , image = None
  442 + , root = None
  443 + , **invalidKeywordArguments
  444 + ):
  445 + """
  446 + Show a box in which a user can enter an integer.
  447 +
  448 + In addition to arguments for msg and title, this function accepts
  449 + integer arguments for "default", "lowerbound", and "upperbound".
  450 +
  451 + The default argument may be None.
  452 +
  453 + When the user enters some text, the text is checked to verify that it
  454 + can be converted to an integer between the lowerbound and upperbound.
  455 +
  456 + If it can be, the integer (not the text) is returned.
  457 +
  458 + If it cannot, then an error msg is displayed, and the integerbox is
  459 + redisplayed.
  460 +
  461 + If the user cancels the operation, None is returned.
  462 +
  463 + NOTE that the "argLowerBound" and "argUpperBound" arguments are no longer
  464 + supported. They have been replaced by "upperbound" and "lowerbound".
  465 + """
  466 + if "argLowerBound" in invalidKeywordArguments:
  467 + raise AssertionError(
  468 + "\nintegerbox no longer supports the 'argLowerBound' argument.\n"
  469 + + "Use 'lowerbound' instead.\n\n")
  470 + if "argUpperBound" in invalidKeywordArguments:
  471 + raise AssertionError(
  472 + "\nintegerbox no longer supports the 'argUpperBound' argument.\n"
  473 + + "Use 'upperbound' instead.\n\n")
  474 +
  475 + if default != "":
  476 + if type(default) != type(1):
  477 + raise AssertionError(
  478 + "integerbox received a non-integer value for "
  479 + + "default of " + dq(str(default)) , "Error")
  480 +
  481 + if type(lowerbound) != type(1):
  482 + raise AssertionError(
  483 + "integerbox received a non-integer value for "
  484 + + "lowerbound of " + dq(str(lowerbound)) , "Error")
  485 +
  486 + if type(upperbound) != type(1):
  487 + raise AssertionError(
  488 + "integerbox received a non-integer value for "
  489 + + "upperbound of " + dq(str(upperbound)) , "Error")
  490 +
  491 + if msg == "":
  492 + msg = ("Enter an integer between " + str(lowerbound)
  493 + + " and "
  494 + + str(upperbound)
  495 + )
  496 +
  497 + while 1:
  498 + reply = enterbox(msg, title, str(default), image=image, root=root)
  499 + if reply == None: return None
  500 +
  501 + try:
  502 + reply = int(reply)
  503 + except:
  504 + msgbox ("The value that you entered:\n\t%s\nis not an integer." % dq(str(reply))
  505 + , "Error")
  506 + continue
  507 +
  508 + if reply < lowerbound:
  509 + msgbox ("The value that you entered is less than the lower bound of "
  510 + + str(lowerbound) + ".", "Error")
  511 + continue
  512 +
  513 + if reply > upperbound:
  514 + msgbox ("The value that you entered is greater than the upper bound of "
  515 + + str(upperbound) + ".", "Error")
  516 + continue
  517 +
  518 + # reply has passed all validation checks.
  519 + # It is an integer between the specified bounds.
  520 + return reply
  521 +
  522 +#-------------------------------------------------------------------
  523 +# multenterbox
  524 +#-------------------------------------------------------------------
  525 +def multenterbox(msg="Fill in values for the fields."
  526 + , title=" "
  527 + , fields=()
  528 + , values=()
  529 + ):
  530 + r"""
  531 + Show screen with multiple data entry fields.
  532 +
  533 + If there are fewer values than names, the list of values is padded with
  534 + empty strings until the number of values is the same as the number of names.
  535 +
  536 + If there are more values than names, the list of values
  537 + is truncated so that there are as many values as names.
  538 +
  539 + Returns a list of the values of the fields,
  540 + or None if the user cancels the operation.
  541 +
  542 + Here is some example code, that shows how values returned from
  543 + multenterbox can be checked for validity before they are accepted::
  544 + ----------------------------------------------------------------------
  545 + msg = "Enter your personal information"
  546 + title = "Credit Card Application"
  547 + fieldNames = ["Name","Street Address","City","State","ZipCode"]
  548 + fieldValues = [] # we start with blanks for the values
  549 + fieldValues = multenterbox(msg,title, fieldNames)
  550 +
  551 + # make sure that none of the fields was left blank
  552 + while 1:
  553 + if fieldValues == None: break
  554 + errmsg = ""
  555 + for i in range(len(fieldNames)):
  556 + if fieldValues[i].strip() == "":
  557 + errmsg += ('"%s" is a required field.\n\n' % fieldNames[i])
  558 + if errmsg == "":
  559 + break # no problems found
  560 + fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues)
  561 +
  562 + writeln("Reply was: %s" % str(fieldValues))
  563 + ----------------------------------------------------------------------
  564 +
  565 + @arg msg: the msg to be displayed.
  566 + @arg title: the window title
  567 + @arg fields: a list of fieldnames.
  568 + @arg values: a list of field values
  569 + """
  570 + return __multfillablebox(msg,title,fields,values,None)
  571 +
  572 +
  573 +#-----------------------------------------------------------------------
  574 +# multpasswordbox
  575 +#-----------------------------------------------------------------------
  576 +def multpasswordbox(msg="Fill in values for the fields."
  577 + , title=" "
  578 + , fields=tuple()
  579 + ,values=tuple()
  580 + ):
  581 + r"""
  582 + Same interface as multenterbox. But in multpassword box,
  583 + the last of the fields is assumed to be a password, and
  584 + is masked with asterisks.
  585 +
  586 + Example
  587 + =======
  588 +
  589 + Here is some example code, that shows how values returned from
  590 + multpasswordbox can be checked for validity before they are accepted::
  591 + msg = "Enter logon information"
  592 + title = "Demo of multpasswordbox"
  593 + fieldNames = ["Server ID", "User ID", "Password"]
  594 + fieldValues = [] # we start with blanks for the values
  595 + fieldValues = multpasswordbox(msg,title, fieldNames)
  596 +
  597 + # make sure that none of the fields was left blank
  598 + while 1:
  599 + if fieldValues == None: break
  600 + errmsg = ""
  601 + for i in range(len(fieldNames)):
  602 + if fieldValues[i].strip() == "":
  603 + errmsg = errmsg + ('"%s" is a required field.\n\n' % fieldNames[i])
  604 + if errmsg == "": break # no problems found
  605 + fieldValues = multpasswordbox(errmsg, title, fieldNames, fieldValues)
  606 +
  607 + writeln("Reply was: %s" % str(fieldValues))
  608 + """
  609 + return __multfillablebox(msg,title,fields,values,"*")
  610 +
  611 +def bindArrows(widget):
  612 + widget.bind("<Down>", tabRight)
  613 + widget.bind("<Up>" , tabLeft)
  614 +
  615 + widget.bind("<Right>",tabRight)
  616 + widget.bind("<Left>" , tabLeft)
  617 +
  618 +def tabRight(event):
  619 + boxRoot.event_generate("<Tab>")
  620 +
  621 +def tabLeft(event):
  622 + boxRoot.event_generate("<Shift-Tab>")
  623 +
  624 +#-----------------------------------------------------------------------
  625 +# __multfillablebox
  626 +#-----------------------------------------------------------------------
  627 +def __multfillablebox(msg="Fill in values for the fields."
  628 + , title=" "
  629 + , fields=()
  630 + , values=()
  631 + , mask = None
  632 + ):
  633 + global boxRoot, __multenterboxText, __multenterboxDefaultText, cancelButton, entryWidget, okButton
  634 +
  635 + choices = ["OK", "Cancel"]
  636 + if len(fields) == 0: return None
  637 +
  638 + fields = list(fields[:]) # convert possible tuples to a list
  639 + values = list(values[:]) # convert possible tuples to a list
  640 +
  641 + if len(values) == len(fields): pass
  642 + elif len(values) > len(fields):
  643 + fields = fields[0:len(values)]
  644 + else:
  645 + while len(values) < len(fields):
  646 + values.append("")
  647 +
  648 + boxRoot = Tk()
  649 +
  650 + boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose )
  651 + boxRoot.title(title)
  652 + boxRoot.iconname('Dialog')
  653 + boxRoot.geometry(rootWindowPosition)
  654 + boxRoot.bind("<Escape>", __multenterboxCancel)
  655 +
  656 + # -------------------- put subframes in the boxRoot --------------------
  657 + messageFrame = Frame(master=boxRoot)
  658 + messageFrame.pack(side=TOP, fill=BOTH)
  659 +
  660 + #-------------------- the msg widget ----------------------------
  661 + messageWidget = Message(messageFrame, width="4.5i", text=msg)
  662 + messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE))
  663 + messageWidget.pack(side=RIGHT, expand=1, fill=BOTH, padx='3m', pady='3m')
  664 +
  665 + global entryWidgets
  666 + entryWidgets = []
  667 +
  668 + lastWidgetIndex = len(fields) - 1
  669 +
  670 + for widgetIndex in range(len(fields)):
  671 + argFieldName = fields[widgetIndex]
  672 + argFieldValue = values[widgetIndex]
  673 + entryFrame = Frame(master=boxRoot)
  674 + entryFrame.pack(side=TOP, fill=BOTH)
  675 +
  676 + # --------- entryWidget ----------------------------------------------
  677 + labelWidget = Label(entryFrame, text=argFieldName)
  678 + labelWidget.pack(side=LEFT)
  679 +
  680 + entryWidget = Entry(entryFrame, width=40,highlightthickness=2)
  681 + entryWidgets.append(entryWidget)
  682 + entryWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,TEXT_ENTRY_FONT_SIZE))
  683 + entryWidget.pack(side=RIGHT, padx="3m")
  684 +
  685 + bindArrows(entryWidget)
  686 +
  687 + entryWidget.bind("<Return>", __multenterboxGetText)
  688 + entryWidget.bind("<Escape>", __multenterboxCancel)
  689 +
  690 + # for the last entryWidget, if this is a multpasswordbox,
  691 + # show the contents as just asterisks
  692 + if widgetIndex == lastWidgetIndex:
  693 + if mask:
  694 + entryWidgets[widgetIndex].configure(show=mask)
  695 +
  696 + # put text into the entryWidget
  697 + entryWidgets[widgetIndex].insert(0,argFieldValue)
  698 + widgetIndex += 1
  699 +
  700 + # ------------------ ok button -------------------------------
  701 + buttonsFrame = Frame(master=boxRoot)
  702 + buttonsFrame.pack(side=BOTTOM, fill=BOTH)
  703 +
  704 + okButton = Button(buttonsFrame, takefocus=1, text="OK")
  705 + bindArrows(okButton)
  706 + okButton.pack(expand=1, side=LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m')
  707 +
  708 + # for the commandButton, bind activation events to the activation event handler
  709 + commandButton = okButton
  710 + handler = __multenterboxGetText
  711 + for selectionEvent in STANDARD_SELECTION_EVENTS:
  712 + commandButton.bind("<%s>" % selectionEvent, handler)
  713 +
  714 +
  715 + # ------------------ cancel button -------------------------------
  716 + cancelButton = Button(buttonsFrame, takefocus=1, text="Cancel")
  717 + bindArrows(cancelButton)
  718 + cancelButton.pack(expand=1, side=RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m')
  719 +
  720 + # for the commandButton, bind activation events to the activation event handler
  721 + commandButton = cancelButton
  722 + handler = __multenterboxCancel
  723 + for selectionEvent in STANDARD_SELECTION_EVENTS:
  724 + commandButton.bind("<%s>" % selectionEvent, handler)
  725 +
  726 +
  727 + # ------------------- time for action! -----------------
  728 + entryWidgets[0].focus_force() # put the focus on the entryWidget
  729 + boxRoot.mainloop() # run it!
  730 +
  731 + # -------- after the run has completed ----------------------------------
  732 + boxRoot.destroy() # button_click didn't destroy boxRoot, so we do it now
  733 + return __multenterboxText
  734 +
  735 +
  736 +#-----------------------------------------------------------------------
  737 +# __multenterboxGetText
  738 +#-----------------------------------------------------------------------
  739 +def __multenterboxGetText(event):
  740 + global __multenterboxText
  741 +
  742 + __multenterboxText = []
  743 + for entryWidget in entryWidgets:
  744 + __multenterboxText.append(entryWidget.get())
  745 + boxRoot.quit()
  746 +
  747 +
  748 +def __multenterboxCancel(event):
  749 + global __multenterboxText
  750 + __multenterboxText = None
  751 + boxRoot.quit()
  752 +
  753 +
  754 +#-------------------------------------------------------------------
  755 +# enterbox
  756 +#-------------------------------------------------------------------
  757 +def enterbox(msg="Enter something."
  758 + , title=" "
  759 + , default=""
  760 + , strip=True
  761 + , image=None
  762 + , root=None
  763 + ):
  764 + """
  765 + Show a box in which a user can enter some text.
  766 +
  767 + You may optionally specify some default text, which will appear in the
  768 + enterbox when it is displayed.
  769 +
  770 + Returns the text that the user entered, or None if he cancels the operation.
  771 +
  772 + By default, enterbox strips its result (i.e. removes leading and trailing
  773 + whitespace). (If you want it not to strip, use keyword argument: strip=False.)
  774 + This makes it easier to test the results of the call::
  775 +
  776 + reply = enterbox(....)
  777 + if reply:
  778 + ...
  779 + else:
  780 + ...
  781 + """
  782 + result = __fillablebox(msg, title, default=default, mask=None,image=image,root=root)
  783 + if result and strip:
  784 + result = result.strip()
  785 + return result
  786 +
  787 +
  788 +def passwordbox(msg="Enter your password."
  789 + , title=" "
  790 + , default=""
  791 + , image=None
  792 + , root=None
  793 + ):
  794 + """
  795 + Show a box in which a user can enter a password.
  796 + The text is masked with asterisks, so the password is not displayed.
  797 + Returns the text that the user entered, or None if he cancels the operation.
  798 + """
  799 + return __fillablebox(msg, title, default, mask="*",image=image,root=root)
  800 +
  801 +
  802 +def __fillablebox(msg
  803 + , title=""
  804 + , default=""
  805 + , mask=None
  806 + , image=None
  807 + , root=None
  808 + ):
  809 + """
  810 + Show a box in which a user can enter some text.
  811 + You may optionally specify some default text, which will appear in the
  812 + enterbox when it is displayed.
  813 + Returns the text that the user entered, or None if he cancels the operation.
  814 + """
  815 +
  816 + global boxRoot, __enterboxText, __enterboxDefaultText
  817 + global cancelButton, entryWidget, okButton
  818 +
  819 + if title == None: title == ""
  820 + if default == None: default = ""
  821 + __enterboxDefaultText = default
  822 + __enterboxText = __enterboxDefaultText
  823 +
  824 + if root:
  825 + root.withdraw()
  826 + boxRoot = Toplevel(master=root)
  827 + boxRoot.withdraw()
  828 + else:
  829 + boxRoot = Tk()
  830 + boxRoot.withdraw()
  831 +
  832 + boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose )
  833 + boxRoot.title(title)
  834 + boxRoot.iconname('Dialog')
  835 + boxRoot.geometry(rootWindowPosition)
  836 + boxRoot.bind("<Escape>", __enterboxCancel)
  837 +
  838 + # ------------- define the messageFrame ---------------------------------
  839 + messageFrame = Frame(master=boxRoot)
  840 + messageFrame.pack(side=TOP, fill=BOTH)
  841 +
  842 + # ------------- define the imageFrame ---------------------------------
  843 + tk_Image = None
  844 + if image:
  845 + imageFilename = os.path.normpath(image)
  846 + junk,ext = os.path.splitext(imageFilename)
  847 +
  848 + if os.path.exists(imageFilename):
  849 + if ext.lower() in [".gif", ".pgm", ".ppm"]:
  850 + tk_Image = PhotoImage(master=boxRoot, file=imageFilename)
  851 + else:
  852 + if PILisLoaded:
  853 + try:
  854 + pil_Image = PILImage.open(imageFilename)
  855 + tk_Image = PILImageTk.PhotoImage(pil_Image, master=boxRoot)
  856 + except:
  857 + msg += ImageErrorMsg % (imageFilename,
  858 + "\nThe Python Imaging Library (PIL) could not convert this file to a displayable image."
  859 + "\n\nPIL reports:\n" + exception_format())
  860 +
  861 + else: # PIL is not loaded
  862 + msg += ImageErrorMsg % (imageFilename,
  863 + "\nI could not import the Python Imaging Library (PIL) to display the image.\n\n"
  864 + "You may need to install PIL\n"
  865 + "(http://www.pythonware.com/products/pil/)\n"
  866 + "to display " + ext + " image files.")
  867 +
  868 + else:
  869 + msg += ImageErrorMsg % (imageFilename, "\nImage file not found.")
  870 +
  871 + if tk_Image:
  872 + imageFrame = Frame(master=boxRoot)
  873 + imageFrame.pack(side=TOP, fill=BOTH)
  874 + label = Label(imageFrame,image=tk_Image)
  875 + label.image = tk_Image # keep a reference!
  876 + label.pack(side=TOP, expand=YES, fill=X, padx='1m', pady='1m')
  877 +
  878 + # ------------- define the buttonsFrame ---------------------------------
  879 + buttonsFrame = Frame(master=boxRoot)
  880 + buttonsFrame.pack(side=TOP, fill=BOTH)
  881 +
  882 +
  883 + # ------------- define the entryFrame ---------------------------------
  884 + entryFrame = Frame(master=boxRoot)
  885 + entryFrame.pack(side=TOP, fill=BOTH)
  886 +
  887 + # ------------- define the buttonsFrame ---------------------------------
  888 + buttonsFrame = Frame(master=boxRoot)
  889 + buttonsFrame.pack(side=TOP, fill=BOTH)
  890 +
  891 + #-------------------- the msg widget ----------------------------
  892 + messageWidget = Message(messageFrame, width="4.5i", text=msg)
  893 + messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE))
  894 + messageWidget.pack(side=RIGHT, expand=1, fill=BOTH, padx='3m', pady='3m')
  895 +
  896 + # --------- entryWidget ----------------------------------------------
  897 + entryWidget = Entry(entryFrame, width=40)
  898 + bindArrows(entryWidget)
  899 + entryWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,TEXT_ENTRY_FONT_SIZE))
  900 + if mask:
  901 + entryWidget.configure(show=mask)
  902 + entryWidget.pack(side=LEFT, padx="3m")
  903 + entryWidget.bind("<Return>", __enterboxGetText)
  904 + entryWidget.bind("<Escape>", __enterboxCancel)
  905 + # put text into the entryWidget
  906 + entryWidget.insert(0,__enterboxDefaultText)
  907 +
  908 + # ------------------ ok button -------------------------------
  909 + okButton = Button(buttonsFrame, takefocus=1, text="OK")
  910 + bindArrows(okButton)
  911 + okButton.pack(expand=1, side=LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m')
  912 +
  913 + # for the commandButton, bind activation events to the activation event handler
  914 + commandButton = okButton
  915 + handler = __enterboxGetText
  916 + for selectionEvent in STANDARD_SELECTION_EVENTS:
  917 + commandButton.bind("<%s>" % selectionEvent, handler)
  918 +
  919 +
  920 + # ------------------ cancel button -------------------------------
  921 + cancelButton = Button(buttonsFrame, takefocus=1, text="Cancel")
  922 + bindArrows(cancelButton)
  923 + cancelButton.pack(expand=1, side=RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m')
  924 +
  925 + # for the commandButton, bind activation events to the activation event handler
  926 + commandButton = cancelButton
  927 + handler = __enterboxCancel
  928 + for selectionEvent in STANDARD_SELECTION_EVENTS:
  929 + commandButton.bind("<%s>" % selectionEvent, handler)
  930 +
  931 + # ------------------- time for action! -----------------
  932 + entryWidget.focus_force() # put the focus on the entryWidget
  933 + boxRoot.deiconify()
  934 + boxRoot.mainloop() # run it!
  935 +
  936 + # -------- after the run has completed ----------------------------------
  937 + if root: root.deiconify()
  938 + boxRoot.destroy() # button_click didn't destroy boxRoot, so we do it now
  939 + return __enterboxText
  940 +
  941 +
  942 +def __enterboxGetText(event):
  943 + global __enterboxText
  944 +
  945 + __enterboxText = entryWidget.get()
  946 + boxRoot.quit()
  947 +
  948 +
  949 +def __enterboxRestore(event):
  950 + global entryWidget
  951 +
  952 + entryWidget.delete(0,len(entryWidget.get()))
  953 + entryWidget.insert(0, __enterboxDefaultText)
  954 +
  955 +
  956 +def __enterboxCancel(event):
  957 + global __enterboxText
  958 +
  959 + __enterboxText = None
  960 + boxRoot.quit()
  961 +
  962 +def denyWindowManagerClose():
  963 + """ don't allow WindowManager close
  964 + """
  965 + x = Tk()
  966 + x.withdraw()
  967 + x.bell()
  968 + x.destroy()
  969 +
  970 +
  971 +
  972 +#-------------------------------------------------------------------
  973 +# multchoicebox
  974 +#-------------------------------------------------------------------
  975 +def multchoicebox(msg="Pick as many items as you like."
  976 + , title=" "
  977 + , choices=()
  978 + , **kwargs
  979 + ):
  980 + """
  981 + Present the user with a list of choices.
  982 + allow him to select multiple items and return them in a list.
  983 + if the user doesn't choose anything from the list, return the empty list.
  984 + return None if he cancelled selection.
  985 +
  986 + @arg msg: the msg to be displayed.
  987 + @arg title: the window title
  988 + @arg choices: a list or tuple of the choices to be displayed
  989 + """
  990 + if len(choices) == 0: choices = ["Program logic error - no choices were specified."]
  991 +
  992 + global __choiceboxMultipleSelect
  993 + __choiceboxMultipleSelect = 1
  994 + return __choicebox(msg, title, choices)
  995 +
  996 +
  997 +#-----------------------------------------------------------------------
  998 +# choicebox
  999 +#-----------------------------------------------------------------------
  1000 +def choicebox(msg="Pick something."
  1001 + , title=" "
  1002 + , choices=()
  1003 + ):
  1004 + """
  1005 + Present the user with a list of choices.
  1006 + return the choice that he selects.
  1007 + return None if he cancels the selection selection.
  1008 +
  1009 + @arg msg: the msg to be displayed.
  1010 + @arg title: the window title
  1011 + @arg choices: a list or tuple of the choices to be displayed
  1012 + """
  1013 + if len(choices) == 0: choices = ["Program logic error - no choices were specified."]
  1014 +
  1015 + global __choiceboxMultipleSelect
  1016 + __choiceboxMultipleSelect = 0
  1017 + return __choicebox(msg,title,choices)
  1018 +
  1019 +
  1020 +#-----------------------------------------------------------------------
  1021 +# __choicebox
  1022 +#-----------------------------------------------------------------------
  1023 +def __choicebox(msg
  1024 + , title
  1025 + , choices
  1026 + ):
  1027 + """
  1028 + internal routine to support choicebox() and multchoicebox()
  1029 + """
  1030 + global boxRoot, __choiceboxResults, choiceboxWidget, defaultText
  1031 + global choiceboxWidget, choiceboxChoices
  1032 + #-------------------------------------------------------------------
  1033 + # If choices is a tuple, we make it a list so we can sort it.
  1034 + # If choices is already a list, we make a new list, so that when
  1035 + # we sort the choices, we don't affect the list object that we
  1036 + # were given.
  1037 + #-------------------------------------------------------------------
  1038 + choices = list(choices[:])
  1039 + if len(choices) == 0:
  1040 + choices = ["Program logic error - no choices were specified."]
  1041 + defaultButtons = ["OK", "Cancel"]
  1042 +
  1043 + # make sure all choices are strings
  1044 + for index in range(len(choices)):
  1045 + choices[index] = str(choices[index])
  1046 +
  1047 + lines_to_show = min(len(choices), 20)
  1048 + lines_to_show = 20
  1049 +
  1050 + if title == None: title = ""
  1051 +
  1052 + # Initialize __choiceboxResults
  1053 + # This is the value that will be returned if the user clicks the close icon
  1054 + __choiceboxResults = None
  1055 +
  1056 + boxRoot = Tk()
  1057 + boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose )
  1058 + screen_width = boxRoot.winfo_screenwidth()
  1059 + screen_height = boxRoot.winfo_screenheight()
  1060 + root_width = int((screen_width * 0.8))
  1061 + root_height = int((screen_height * 0.5))
  1062 + root_xpos = int((screen_width * 0.1))
  1063 + root_ypos = int((screen_height * 0.05))
  1064 +
  1065 + boxRoot.title(title)
  1066 + boxRoot.iconname('Dialog')
  1067 + rootWindowPosition = "+0+0"
  1068 + boxRoot.geometry(rootWindowPosition)
  1069 + boxRoot.expand=NO
  1070 + boxRoot.minsize(root_width, root_height)
  1071 + rootWindowPosition = "+" + str(root_xpos) + "+" + str(root_ypos)
  1072 + boxRoot.geometry(rootWindowPosition)
  1073 +
  1074 + # ---------------- put the frames in the window -----------------------------------------
  1075 + message_and_buttonsFrame = Frame(master=boxRoot)
  1076 + message_and_buttonsFrame.pack(side=TOP, fill=X, expand=NO)
  1077 +
  1078 + messageFrame = Frame(message_and_buttonsFrame)
  1079 + messageFrame.pack(side=LEFT, fill=X, expand=YES)
  1080 + #messageFrame.pack(side=TOP, fill=X, expand=YES)
  1081 +
  1082 + buttonsFrame = Frame(message_and_buttonsFrame)
  1083 + buttonsFrame.pack(side=RIGHT, expand=NO, pady=0)
  1084 + #buttonsFrame.pack(side=TOP, expand=YES, pady=0)
  1085 +
  1086 + choiceboxFrame = Frame(master=boxRoot)
  1087 + choiceboxFrame.pack(side=BOTTOM, fill=BOTH, expand=YES)
  1088 +
  1089 + # -------------------------- put the widgets in the frames ------------------------------
  1090 +
  1091 + # ---------- put a msg widget in the msg frame-------------------
  1092 + messageWidget = Message(messageFrame, anchor=NW, text=msg, width=int(root_width * 0.9))
  1093 + messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE))
  1094 + messageWidget.pack(side=LEFT, expand=YES, fill=BOTH, padx='1m', pady='1m')
  1095 +
  1096 + # -------- put the choiceboxWidget in the choiceboxFrame ---------------------------
  1097 + choiceboxWidget = Listbox(choiceboxFrame
  1098 + , height=lines_to_show
  1099 + , borderwidth="1m"
  1100 + , relief="flat"
  1101 + , bg="white"
  1102 + )
  1103 +
  1104 + if __choiceboxMultipleSelect:
  1105 + choiceboxWidget.configure(selectmode=MULTIPLE)
  1106 +
  1107 + choiceboxWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE))
  1108 +
  1109 + # add a vertical scrollbar to the frame
  1110 + rightScrollbar = Scrollbar(choiceboxFrame, orient=VERTICAL, command=choiceboxWidget.yview)
  1111 + choiceboxWidget.configure(yscrollcommand = rightScrollbar.set)
  1112 +
  1113 + # add a horizontal scrollbar to the frame
  1114 + bottomScrollbar = Scrollbar(choiceboxFrame, orient=HORIZONTAL, command=choiceboxWidget.xview)
  1115 + choiceboxWidget.configure(xscrollcommand = bottomScrollbar.set)
  1116 +
  1117 + # pack the Listbox and the scrollbars. Note that although we must define
  1118 + # the textArea first, we must pack it last, so that the bottomScrollbar will
  1119 + # be located properly.
  1120 +
  1121 + bottomScrollbar.pack(side=BOTTOM, fill = X)
  1122 + rightScrollbar.pack(side=RIGHT, fill = Y)
  1123 +
  1124 + choiceboxWidget.pack(side=LEFT, padx="1m", pady="1m", expand=YES, fill=BOTH)
  1125 +
  1126 + #---------------------------------------------------
  1127 + # sort the choices
  1128 + # eliminate duplicates
  1129 + # put the choices into the choiceboxWidget
  1130 + #---------------------------------------------------
  1131 + for index in range(len(choices)):
  1132 + choices[index] = str(choices[index])
  1133 +
  1134 + if runningPython3:
  1135 + choices.sort(key=str.lower)
  1136 + else:
  1137 + choices.sort( lambda x,y: cmp(x.lower(), y.lower())) # case-insensitive sort
  1138 +
  1139 + lastInserted = None
  1140 + choiceboxChoices = []
  1141 + for choice in choices:
  1142 + if choice == lastInserted: pass
  1143 + else:
  1144 + choiceboxWidget.insert(END, choice)
  1145 + choiceboxChoices.append(choice)
  1146 + lastInserted = choice
  1147 +
  1148 + boxRoot.bind('<Any-Key>', KeyboardListener)
  1149 +
  1150 + # put the buttons in the buttonsFrame
  1151 + if len(choices) > 0:
  1152 + okButton = Button(buttonsFrame, takefocus=YES, text="OK", height=1, width=6)
  1153 + bindArrows(okButton)
  1154 + okButton.pack(expand=NO, side=TOP, padx='2m', pady='1m', ipady="1m", ipadx="2m")
  1155 +
  1156 + # for the commandButton, bind activation events to the activation event handler
  1157 + commandButton = okButton
  1158 + handler = __choiceboxGetChoice
  1159 + for selectionEvent in STANDARD_SELECTION_EVENTS:
  1160 + commandButton.bind("<%s>" % selectionEvent, handler)
  1161 +
  1162 + # now bind the keyboard events
  1163 + choiceboxWidget.bind("<Return>", __choiceboxGetChoice)
  1164 + choiceboxWidget.bind("<Double-Button-1>", __choiceboxGetChoice)
  1165 + else:
  1166 + # now bind the keyboard events
  1167 + choiceboxWidget.bind("<Return>", __choiceboxCancel)
  1168 + choiceboxWidget.bind("<Double-Button-1>", __choiceboxCancel)
  1169 +
  1170 + cancelButton = Button(buttonsFrame, takefocus=YES, text="Cancel", height=1, width=6)
  1171 + bindArrows(cancelButton)
  1172 + cancelButton.pack(expand=NO, side=BOTTOM, padx='2m', pady='1m', ipady="1m", ipadx="2m")
  1173 +
  1174 + # for the commandButton, bind activation events to the activation event handler
  1175 + commandButton = cancelButton
  1176 + handler = __choiceboxCancel
  1177 + for selectionEvent in STANDARD_SELECTION_EVENTS:
  1178 + commandButton.bind("<%s>" % selectionEvent, handler)
  1179 +
  1180 +
  1181 + # add special buttons for multiple select features
  1182 + if len(choices) > 0 and __choiceboxMultipleSelect:
  1183 + selectionButtonsFrame = Frame(messageFrame)
  1184 + selectionButtonsFrame.pack(side=RIGHT, fill=Y, expand=NO)
  1185 +
  1186 + selectAllButton = Button(selectionButtonsFrame, text="Select All", height=1, width=6)
  1187 + bindArrows(selectAllButton)
  1188 +
  1189 + selectAllButton.bind("<Button-1>",__choiceboxSelectAll)
  1190 + selectAllButton.pack(expand=NO, side=TOP, padx='2m', pady='1m', ipady="1m", ipadx="2m")
  1191 +
  1192 + clearAllButton = Button(selectionButtonsFrame, text="Clear All", height=1, width=6)
  1193 + bindArrows(clearAllButton)
  1194 + clearAllButton.bind("<Button-1>",__choiceboxClearAll)
  1195 + clearAllButton.pack(expand=NO, side=TOP, padx='2m', pady='1m', ipady="1m", ipadx="2m")
  1196 +
  1197 +
  1198 + # -------------------- bind some keyboard events ----------------------------
  1199 + boxRoot.bind("<Escape>", __choiceboxCancel)
  1200 +
  1201 + # --------------------- the action begins -----------------------------------
  1202 + # put the focus on the choiceboxWidget, and the select highlight on the first item
  1203 + choiceboxWidget.select_set(0)
  1204 + choiceboxWidget.focus_force()
  1205 +
  1206 + # --- run it! -----
  1207 + boxRoot.mainloop()
  1208 +
  1209 + boxRoot.destroy()
  1210 + return __choiceboxResults
  1211 +
  1212 +
  1213 +def __choiceboxGetChoice(event):
  1214 + global boxRoot, __choiceboxResults, choiceboxWidget
  1215 +
  1216 + if __choiceboxMultipleSelect:
  1217 + __choiceboxResults = [choiceboxWidget.get(index) for index in choiceboxWidget.curselection()]
  1218 +
  1219 + else:
  1220 + choice_index = choiceboxWidget.curselection()
  1221 + __choiceboxResults = choiceboxWidget.get(choice_index)
  1222 +
  1223 + # writeln("Debugging> mouse-event=", event, " event.type=", event.type)
  1224 + # writeln("Debugging> choice=", choice_index, __choiceboxResults)
  1225 + boxRoot.quit()
  1226 +
  1227 +
  1228 +def __choiceboxSelectAll(event):
  1229 + global choiceboxWidget, choiceboxChoices
  1230 +
  1231 + choiceboxWidget.selection_set(0, len(choiceboxChoices)-1)
  1232 +
  1233 +def __choiceboxClearAll(event):
  1234 + global choiceboxWidget, choiceboxChoices
  1235 +
  1236 + choiceboxWidget.selection_clear(0, len(choiceboxChoices)-1)
  1237 +
  1238 +
  1239 +
  1240 +def __choiceboxCancel(event):
  1241 + global boxRoot, __choiceboxResults
  1242 +
  1243 + __choiceboxResults = None
  1244 + boxRoot.quit()
  1245 +
  1246 +
  1247 +def KeyboardListener(event):
  1248 + global choiceboxChoices, choiceboxWidget
  1249 + key = event.keysym
  1250 + if len(key) <= 1:
  1251 + if key in string.printable:
  1252 + # Find the key in the list.
  1253 + # before we clear the list, remember the selected member
  1254 + try:
  1255 + start_n = int(choiceboxWidget.curselection()[0])
  1256 + except IndexError:
  1257 + start_n = -1
  1258 +
  1259 + ## clear the selection.
  1260 + choiceboxWidget.selection_clear(0, 'end')
  1261 +
  1262 + ## start from previous selection +1
  1263 + for n in range(start_n+1, len(choiceboxChoices)):
  1264 + item = choiceboxChoices[n]
  1265 + if item[0].lower() == key.lower():
  1266 + choiceboxWidget.selection_set(first=n)
  1267 + choiceboxWidget.see(n)
  1268 + return
  1269 + else:
  1270 + # has not found it so loop from top
  1271 + for n in range(len(choiceboxChoices)):
  1272 + item = choiceboxChoices[n]
  1273 + if item[0].lower() == key.lower():
  1274 + choiceboxWidget.selection_set(first = n)
  1275 + choiceboxWidget.see(n)
  1276 + return
  1277 +
  1278 + # nothing matched -- we'll look for the next logical choice
  1279 + for n in range(len(choiceboxChoices)):
  1280 + item = choiceboxChoices[n]
  1281 + if item[0].lower() > key.lower():
  1282 + if n > 0:
  1283 + choiceboxWidget.selection_set(first = (n-1))
  1284 + else:
  1285 + choiceboxWidget.selection_set(first = 0)
  1286 + choiceboxWidget.see(n)
  1287 + return
  1288 +
  1289 + # still no match (nothing was greater than the key)
  1290 + # we set the selection to the first item in the list
  1291 + lastIndex = len(choiceboxChoices)-1
  1292 + choiceboxWidget.selection_set(first = lastIndex)
  1293 + choiceboxWidget.see(lastIndex)
  1294 + return
  1295 +
  1296 +#-----------------------------------------------------------------------
  1297 +# exception_format
  1298 +#-----------------------------------------------------------------------
  1299 +def exception_format():
  1300 + """
  1301 + Convert exception info into a string suitable for display.
  1302 + """
  1303 + return "".join(traceback.format_exception(
  1304 + sys.exc_info()[0]
  1305 + , sys.exc_info()[1]
  1306 + , sys.exc_info()[2]
  1307 + ))
  1308 +
  1309 +#-----------------------------------------------------------------------
  1310 +# exceptionbox
  1311 +#-----------------------------------------------------------------------
  1312 +def exceptionbox(msg=None, title=None):
  1313 + """
  1314 + Display a box that gives information about
  1315 + an exception that has just been raised.
  1316 +
  1317 + The caller may optionally pass in a title for the window, or a
  1318 + msg to accompany the error information.
  1319 +
  1320 + Note that you do not need to (and cannot) pass an exception object
  1321 + as an argument. The latest exception will automatically be used.
  1322 + """
  1323 + if title == None: title = "Error Report"
  1324 + if msg == None:
  1325 + msg = "An error (exception) has occurred in the program."
  1326 +
  1327 + codebox(msg, title, exception_format())
  1328 +
  1329 +#-------------------------------------------------------------------
  1330 +# codebox
  1331 +#-------------------------------------------------------------------
  1332 +
  1333 +def codebox(msg=""
  1334 + , title=" "
  1335 + , text=""
  1336 + ):
  1337 + """
  1338 + Display some text in a monospaced font, with no line wrapping.
  1339 + This function is suitable for displaying code and text that is
  1340 + formatted using spaces.
  1341 +
  1342 + The text parameter should be a string, or a list or tuple of lines to be
  1343 + displayed in the textbox.
  1344 + """
  1345 + return textbox(msg, title, text, codebox=1 )
  1346 +
  1347 +#-------------------------------------------------------------------
  1348 +# textbox
  1349 +#-------------------------------------------------------------------
  1350 +def textbox(msg=""
  1351 + , title=" "
  1352 + , text=""
  1353 + , codebox=0
  1354 + ):
  1355 + """
  1356 + Display some text in a proportional font with line wrapping at word breaks.
  1357 + This function is suitable for displaying general written text.
  1358 +
  1359 + The text parameter should be a string, or a list or tuple of lines to be
  1360 + displayed in the textbox.
  1361 + """
  1362 +
  1363 + if msg == None: msg = ""
  1364 + if title == None: title = ""
  1365 +
  1366 + global boxRoot, __replyButtonText, __widgetTexts, buttonsFrame
  1367 + global rootWindowPosition
  1368 + choices = ["OK"]
  1369 + __replyButtonText = choices[0]
  1370 +
  1371 +
  1372 + boxRoot = Tk()
  1373 +
  1374 + boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose )
  1375 +
  1376 + screen_width = boxRoot.winfo_screenwidth()
  1377 + screen_height = boxRoot.winfo_screenheight()
  1378 + root_width = int((screen_width * 0.8))
  1379 + root_height = int((screen_height * 0.5))
  1380 + root_xpos = int((screen_width * 0.1))
  1381 + root_ypos = int((screen_height * 0.05))
  1382 +
  1383 + boxRoot.title(title)
  1384 + boxRoot.iconname('Dialog')
  1385 + rootWindowPosition = "+0+0"
  1386 + boxRoot.geometry(rootWindowPosition)
  1387 + boxRoot.expand=NO
  1388 + boxRoot.minsize(root_width, root_height)
  1389 + rootWindowPosition = "+" + str(root_xpos) + "+" + str(root_ypos)
  1390 + boxRoot.geometry(rootWindowPosition)
  1391 +
  1392 + mainframe = Frame(master=boxRoot)
  1393 + mainframe.pack(side=TOP, fill=BOTH, expand=YES)
  1394 +
  1395 + # ---- put frames in the window -----------------------------------
  1396 + # we pack the textboxFrame first, so it will expand first
  1397 + textboxFrame = Frame(mainframe, borderwidth=3)
  1398 + textboxFrame.pack(side=BOTTOM , fill=BOTH, expand=YES)
  1399 +
  1400 + message_and_buttonsFrame = Frame(mainframe)
  1401 + message_and_buttonsFrame.pack(side=TOP, fill=X, expand=NO)
  1402 +
  1403 + messageFrame = Frame(message_and_buttonsFrame)
  1404 + messageFrame.pack(side=LEFT, fill=X, expand=YES)
  1405 +
  1406 + buttonsFrame = Frame(message_and_buttonsFrame)
  1407 + buttonsFrame.pack(side=RIGHT, expand=NO)
  1408 +
  1409 + # -------------------- put widgets in the frames --------------------
  1410 +
  1411 + # put a textArea in the top frame
  1412 + if codebox:
  1413 + character_width = int((root_width * 0.6) / MONOSPACE_FONT_SIZE)
  1414 + textArea = Text(textboxFrame,height=25,width=character_width, padx="2m", pady="1m")
  1415 + textArea.configure(wrap=NONE)
  1416 + textArea.configure(font=(MONOSPACE_FONT_FAMILY, MONOSPACE_FONT_SIZE))
  1417 +
  1418 + else:
  1419 + character_width = int((root_width * 0.6) / MONOSPACE_FONT_SIZE)
  1420 + textArea = Text(
  1421 + textboxFrame
  1422 + , height=25
  1423 + , width=character_width
  1424 + , padx="2m"
  1425 + , pady="1m"
  1426 + )
  1427 + textArea.configure(wrap=WORD)
  1428 + textArea.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE))
  1429 +
  1430 +
  1431 + # some simple keybindings for scrolling
  1432 + mainframe.bind("<Next>" , textArea.yview_scroll( 1,PAGES))
  1433 + mainframe.bind("<Prior>", textArea.yview_scroll(-1,PAGES))
  1434 +
  1435 + mainframe.bind("<Right>", textArea.xview_scroll( 1,PAGES))
  1436 + mainframe.bind("<Left>" , textArea.xview_scroll(-1,PAGES))
  1437 +
  1438 + mainframe.bind("<Down>", textArea.yview_scroll( 1,UNITS))
  1439 + mainframe.bind("<Up>" , textArea.yview_scroll(-1,UNITS))
  1440 +
  1441 +
  1442 + # add a vertical scrollbar to the frame
  1443 + rightScrollbar = Scrollbar(textboxFrame, orient=VERTICAL, command=textArea.yview)
  1444 + textArea.configure(yscrollcommand = rightScrollbar.set)
  1445 +
  1446 + # add a horizontal scrollbar to the frame
  1447 + bottomScrollbar = Scrollbar(textboxFrame, orient=HORIZONTAL, command=textArea.xview)
  1448 + textArea.configure(xscrollcommand = bottomScrollbar.set)
  1449 +
  1450 + # pack the textArea and the scrollbars. Note that although we must define
  1451 + # the textArea first, we must pack it last, so that the bottomScrollbar will
  1452 + # be located properly.
  1453 +
  1454 + # Note that we need a bottom scrollbar only for code.
  1455 + # Text will be displayed with wordwrap, so we don't need to have a horizontal
  1456 + # scroll for it.
  1457 + if codebox:
  1458 + bottomScrollbar.pack(side=BOTTOM, fill=X)
  1459 + rightScrollbar.pack(side=RIGHT, fill=Y)
  1460 +
  1461 + textArea.pack(side=LEFT, fill=BOTH, expand=YES)
  1462 +
  1463 +
  1464 + # ---------- put a msg widget in the msg frame-------------------
  1465 + messageWidget = Message(messageFrame, anchor=NW, text=msg, width=int(root_width * 0.9))
  1466 + messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE))
  1467 + messageWidget.pack(side=LEFT, expand=YES, fill=BOTH, padx='1m', pady='1m')
  1468 +
  1469 + # put the buttons in the buttonsFrame
  1470 + okButton = Button(buttonsFrame, takefocus=YES, text="OK", height=1, width=6)
  1471 + okButton.pack(expand=NO, side=TOP, padx='2m', pady='1m', ipady="1m", ipadx="2m")
  1472 +
  1473 + # for the commandButton, bind activation events to the activation event handler
  1474 + commandButton = okButton
  1475 + handler = __textboxOK
  1476 + for selectionEvent in ["Return","Button-1","Escape"]:
  1477 + commandButton.bind("<%s>" % selectionEvent, handler)
  1478 +
  1479 +
  1480 + # ----------------- the action begins ----------------------------------------
  1481 + try:
  1482 + # load the text into the textArea
  1483 + if type(text) == type("abc"): pass
  1484 + else:
  1485 + try:
  1486 + text = "".join(text) # convert a list or a tuple to a string
  1487 + except:
  1488 + msgbox("Exception when trying to convert "+ str(type(text)) + " to text in textArea")
  1489 + sys.exit(16)
  1490 + textArea.insert(END,text, "normal")
  1491 +
  1492 + except:
  1493 + msgbox("Exception when trying to load the textArea.")
  1494 + sys.exit(16)
  1495 +
  1496 + try:
  1497 + okButton.focus_force()
  1498 + except:
  1499 + msgbox("Exception when trying to put focus on okButton.")
  1500 + sys.exit(16)
  1501 +
  1502 + boxRoot.mainloop()
  1503 +
  1504 + # this line MUST go before the line that destroys boxRoot
  1505 + areaText = textArea.get(0.0,END)
  1506 + boxRoot.destroy()
  1507 + return areaText # return __replyButtonText
  1508 +
  1509 +#-------------------------------------------------------------------
  1510 +# __textboxOK
  1511 +#-------------------------------------------------------------------
  1512 +def __textboxOK(event):
  1513 + global boxRoot
  1514 + boxRoot.quit()
  1515 +
  1516 +
  1517 +
  1518 +#-------------------------------------------------------------------
  1519 +# diropenbox
  1520 +#-------------------------------------------------------------------
  1521 +def diropenbox(msg=None
  1522 + , title=None
  1523 + , default=None
  1524 + ):
  1525 + """
  1526 + A dialog to get a directory name.
  1527 + Note that the msg argument, if specified, is ignored.
  1528 +
  1529 + Returns the name of a directory, or None if user chose to cancel.
  1530 +
  1531 + If the "default" argument specifies a directory name, and that
  1532 + directory exists, then the dialog box will start with that directory.
  1533 + """
  1534 + title=getFileDialogTitle(msg,title)
  1535 + localRoot = Tk()
  1536 + localRoot.withdraw()
  1537 + if not default: default = None
  1538 + f = tk_FileDialog.askdirectory(
  1539 + parent=localRoot
  1540 + , title=title
  1541 + , initialdir=default
  1542 + , initialfile=None
  1543 + )
  1544 + localRoot.destroy()
  1545 + if not f: return None
  1546 + return os.path.normpath(f)
  1547 +
  1548 +
  1549 +
  1550 +#-------------------------------------------------------------------
  1551 +# getFileDialogTitle
  1552 +#-------------------------------------------------------------------
  1553 +def getFileDialogTitle(msg
  1554 + , title
  1555 + ):
  1556 + if msg and title: return "%s - %s" % (title,msg)
  1557 + if msg and not title: return str(msg)
  1558 + if title and not msg: return str(title)
  1559 + return None # no message and no title
  1560 +
  1561 +#-------------------------------------------------------------------
  1562 +# class FileTypeObject for use with fileopenbox
  1563 +#-------------------------------------------------------------------
  1564 +class FileTypeObject:
  1565 + def __init__(self,filemask):
  1566 + if len(filemask) == 0:
  1567 + raise AssertionError('Filetype argument is empty.')
  1568 +
  1569 + self.masks = []
  1570 +
  1571 + if type(filemask) == type("abc"): # a string
  1572 + self.initializeFromString(filemask)
  1573 +
  1574 + elif type(filemask) == type([]): # a list
  1575 + if len(filemask) < 2:
  1576 + raise AssertionError('Invalid filemask.\n'
  1577 + +'List contains less than 2 members: "%s"' % filemask)
  1578 + else:
  1579 + self.name = filemask[-1]
  1580 + self.masks = list(filemask[:-1] )
  1581 + else:
  1582 + raise AssertionError('Invalid filemask: "%s"' % filemask)
  1583 +
  1584 + def __eq__(self,other):
  1585 + if self.name == other.name: return True
  1586 + return False
  1587 +
  1588 + def add(self,other):
  1589 + for mask in other.masks:
  1590 + if mask in self.masks: pass
  1591 + else: self.masks.append(mask)
  1592 +
  1593 + def toTuple(self):
  1594 + return (self.name,tuple(self.masks))
  1595 +
  1596 + def isAll(self):
  1597 + if self.name == "All files": return True
  1598 + return False
  1599 +
  1600 + def initializeFromString(self, filemask):
  1601 + # remove everything except the extension from the filemask
  1602 + self.ext = os.path.splitext(filemask)[1]
  1603 + if self.ext == "" : self.ext = ".*"
  1604 + if self.ext == ".": self.ext = ".*"
  1605 + self.name = self.getName()
  1606 + self.masks = ["*" + self.ext]
  1607 +
  1608 + def getName(self):
  1609 + e = self.ext
  1610 + if e == ".*" : return "All files"
  1611 + if e == ".txt": return "Text files"
  1612 + if e == ".py" : return "Python files"
  1613 + if e == ".pyc" : return "Python files"
  1614 + if e == ".xls": return "Excel files"
  1615 + if e.startswith("."):
  1616 + return e[1:].upper() + " files"
  1617 + return e.upper() + " files"
  1618 +
  1619 +
  1620 +#-------------------------------------------------------------------
  1621 +# fileopenbox
  1622 +#-------------------------------------------------------------------
  1623 +def fileopenbox(msg=None
  1624 + , title=None
  1625 + , default="*"
  1626 + , filetypes=None
  1627 + ):
  1628 + """
  1629 + A dialog to get a file name.
  1630 +
  1631 + About the "default" argument
  1632 + ============================
  1633 + The "default" argument specifies a filepath that (normally)
  1634 + contains one or more wildcards.
  1635 + fileopenbox will display only files that match the default filepath.
  1636 + If omitted, defaults to "*" (all files in the current directory).
  1637 +
  1638 + WINDOWS EXAMPLE::
  1639 + ...default="c:/myjunk/*.py"
  1640 + will open in directory c:\myjunk\ and show all Python files.
  1641 +
  1642 + WINDOWS EXAMPLE::
  1643 + ...default="c:/myjunk/test*.py"
  1644 + will open in directory c:\myjunk\ and show all Python files
  1645 + whose names begin with "test".
  1646 +
  1647 +
  1648 + Note that on Windows, fileopenbox automatically changes the path
  1649 + separator to the Windows path separator (backslash).
  1650 +
  1651 + About the "filetypes" argument
  1652 + ==============================
  1653 + If specified, it should contain a list of items,
  1654 + where each item is either::
  1655 + - a string containing a filemask # e.g. "*.txt"
  1656 + - a list of strings, where all of the strings except the last one
  1657 + are filemasks (each beginning with "*.",
  1658 + such as "*.txt" for text files, "*.py" for Python files, etc.).
  1659 + and the last string contains a filetype description
  1660 +
  1661 + EXAMPLE::
  1662 + filetypes = ["*.css", ["*.htm", "*.html", "HTML files"] ]
  1663 +
  1664 + NOTE THAT
  1665 + =========
  1666 +
  1667 + If the filetypes list does not contain ("All files","*"),
  1668 + it will be added.
  1669 +
  1670 + If the filetypes list does not contain a filemask that includes
  1671 + the extension of the "default" argument, it will be added.
  1672 + For example, if default="*abc.py"
  1673 + and no filetypes argument was specified, then
  1674 + "*.py" will automatically be added to the filetypes argument.
  1675 +
  1676 + @rtype: string or None
  1677 + @return: the name of a file, or None if user chose to cancel
  1678 +
  1679 + @arg msg: the msg to be displayed.
  1680 + @arg title: the window title
  1681 + @arg default: filepath with wildcards
  1682 + @arg filetypes: filemasks that a user can choose, e.g. "*.txt"
  1683 + """
  1684 + localRoot = Tk()
  1685 + localRoot.withdraw()
  1686 +
  1687 + initialbase, initialfile, initialdir, filetypes = fileboxSetup(default,filetypes)
  1688 +
  1689 + #------------------------------------------------------------
  1690 + # if initialfile contains no wildcards; we don't want an
  1691 + # initial file. It won't be used anyway.
  1692 + # Also: if initialbase is simply "*", we don't want an
  1693 + # initialfile; it is not doing any useful work.
  1694 + #------------------------------------------------------------
  1695 + if (initialfile.find("*") < 0) and (initialfile.find("?") < 0):
  1696 + initialfile = None
  1697 + elif initialbase == "*":
  1698 + initialfile = None
  1699 +
  1700 + f = tk_FileDialog.askopenfilename(parent=localRoot
  1701 + , title=getFileDialogTitle(msg,title)
  1702 + , initialdir=initialdir
  1703 + , initialfile=initialfile
  1704 + , filetypes=filetypes
  1705 + )
  1706 +
  1707 + localRoot.destroy()
  1708 +
  1709 + if not f: return None
  1710 + return os.path.normpath(f)
  1711 +
  1712 +
  1713 +#-------------------------------------------------------------------
  1714 +# filesavebox
  1715 +#-------------------------------------------------------------------
  1716 +def filesavebox(msg=None
  1717 + , title=None
  1718 + , default=""
  1719 + , filetypes=None
  1720 + ):
  1721 + """
  1722 + A file to get the name of a file to save.
  1723 + Returns the name of a file, or None if user chose to cancel.
  1724 +
  1725 + The "default" argument should contain a filename (i.e. the
  1726 + current name of the file to be saved). It may also be empty,
  1727 + or contain a filemask that includes wildcards.
  1728 +
  1729 + The "filetypes" argument works like the "filetypes" argument to
  1730 + fileopenbox.
  1731 + """
  1732 +
  1733 + localRoot = Tk()
  1734 + localRoot.withdraw()
  1735 +
  1736 + initialbase, initialfile, initialdir, filetypes = fileboxSetup(default,filetypes)
  1737 +
  1738 + f = tk_FileDialog.asksaveasfilename(parent=localRoot
  1739 + , title=getFileDialogTitle(msg,title)
  1740 + , initialfile=initialfile
  1741 + , initialdir=initialdir
  1742 + , filetypes=filetypes
  1743 + )
  1744 + localRoot.destroy()
  1745 + if not f: return None
  1746 + return os.path.normpath(f)
  1747 +
  1748 +
  1749 +#-------------------------------------------------------------------
  1750 +#
  1751 +# fileboxSetup
  1752 +#
  1753 +#-------------------------------------------------------------------
  1754 +def fileboxSetup(default,filetypes):
  1755 + if not default: default = os.path.join(".","*")
  1756 + initialdir, initialfile = os.path.split(default)
  1757 + if not initialdir : initialdir = "."
  1758 + if not initialfile: initialfile = "*"
  1759 + initialbase, initialext = os.path.splitext(initialfile)
  1760 + initialFileTypeObject = FileTypeObject(initialfile)
  1761 +
  1762 + allFileTypeObject = FileTypeObject("*")
  1763 + ALL_filetypes_was_specified = False
  1764 +
  1765 + if not filetypes: filetypes= []
  1766 + filetypeObjects = []
  1767 +
  1768 + for filemask in filetypes:
  1769 + fto = FileTypeObject(filemask)
  1770 +
  1771 + if fto.isAll():
  1772 + ALL_filetypes_was_specified = True # remember this
  1773 +
  1774 + if fto == initialFileTypeObject:
  1775 + initialFileTypeObject.add(fto) # add fto to initialFileTypeObject
  1776 + else:
  1777 + filetypeObjects.append(fto)
  1778 +
  1779 + #------------------------------------------------------------------
  1780 + # make sure that the list of filetypes includes the ALL FILES type.
  1781 + #------------------------------------------------------------------
  1782 + if ALL_filetypes_was_specified:
  1783 + pass
  1784 + elif allFileTypeObject == initialFileTypeObject:
  1785 + pass
  1786 + else:
  1787 + filetypeObjects.insert(0,allFileTypeObject)
  1788 + #------------------------------------------------------------------
  1789 + # Make sure that the list includes the initialFileTypeObject
  1790 + # in the position in the list that will make it the default.
  1791 + # This changed between Python version 2.5 and 2.6
  1792 + #------------------------------------------------------------------
  1793 + if len(filetypeObjects) == 0:
  1794 + filetypeObjects.append(initialFileTypeObject)
  1795 +
  1796 + if initialFileTypeObject in (filetypeObjects[0], filetypeObjects[-1]):
  1797 + pass
  1798 + else:
  1799 + if runningPython26:
  1800 + filetypeObjects.append(initialFileTypeObject)
  1801 + else:
  1802 + filetypeObjects.insert(0,initialFileTypeObject)
  1803 +
  1804 + filetypes = [fto.toTuple() for fto in filetypeObjects]
  1805 +
  1806 + return initialbase, initialfile, initialdir, filetypes
  1807 +
  1808 +#-------------------------------------------------------------------
  1809 +# utility routines
  1810 +#-------------------------------------------------------------------
  1811 +# These routines are used by several other functions in the EasyGui module.
  1812 +
  1813 +def __buttonEvent(event):
  1814 + """
  1815 + Handle an event that is generated by a person clicking a button.
  1816 + """
  1817 + global boxRoot, __widgetTexts, __replyButtonText
  1818 + __replyButtonText = __widgetTexts[event.widget]
  1819 + boxRoot.quit() # quit the main loop
  1820 +
  1821 +
  1822 +def __put_buttons_in_buttonframe(choices):
  1823 + """Put the buttons in the buttons frame
  1824 + """
  1825 + global __widgetTexts, __firstWidget, buttonsFrame
  1826 +
  1827 + __firstWidget = None
  1828 + __widgetTexts = {}
  1829 +
  1830 + i = 0
  1831 +
  1832 + for buttonText in choices:
  1833 + tempButton = Button(buttonsFrame, takefocus=1, text=buttonText)
  1834 + bindArrows(tempButton)
  1835 + tempButton.pack(expand=YES, side=LEFT, padx='1m', pady='1m', ipadx='2m', ipady='1m')
  1836 +
  1837 + # remember the text associated with this widget
  1838 + __widgetTexts[tempButton] = buttonText
  1839 +
  1840 + # remember the first widget, so we can put the focus there
  1841 + if i == 0:
  1842 + __firstWidget = tempButton
  1843 + i = 1
  1844 +
  1845 + # for the commandButton, bind activation events to the activation event handler
  1846 + commandButton = tempButton
  1847 + handler = __buttonEvent
  1848 + for selectionEvent in STANDARD_SELECTION_EVENTS:
  1849 + commandButton.bind("<%s>" % selectionEvent, handler)
  1850 +
  1851 +#-----------------------------------------------------------------------
  1852 +#
  1853 +# class EgStore
  1854 +#
  1855 +#-----------------------------------------------------------------------
  1856 +class EgStore:
  1857 + r"""
  1858 +A class to support persistent storage.
  1859 +
  1860 +You can use EgStore to support the storage and retrieval
  1861 +of user settings for an EasyGui application.
  1862 +
  1863 +
  1864 +# Example A
  1865 +#-----------------------------------------------------------------------
  1866 +# define a class named Settings as a subclass of EgStore
  1867 +#-----------------------------------------------------------------------
  1868 +class Settings(EgStore):
  1869 +::
  1870 + def __init__(self, filename): # filename is required
  1871 + #-------------------------------------------------
  1872 + # Specify default/initial values for variables that
  1873 + # this particular application wants to remember.
  1874 + #-------------------------------------------------
  1875 + self.userId = ""
  1876 + self.targetServer = ""
  1877 +
  1878 + #-------------------------------------------------
  1879 + # For subclasses of EgStore, these must be
  1880 + # the last two statements in __init__
  1881 + #-------------------------------------------------
  1882 + self.filename = filename # this is required
  1883 + self.restore() # restore values from the storage file if possible
  1884 +
  1885 +
  1886 +
  1887 +# Example B
  1888 +#-----------------------------------------------------------------------
  1889 +# create settings, a persistent Settings object
  1890 +#-----------------------------------------------------------------------
  1891 +settingsFile = "myApp_settings.txt"
  1892 +settings = Settings(settingsFile)
  1893 +
  1894 +user = "obama_barak"
  1895 +server = "whitehouse1"
  1896 +settings.userId = user
  1897 +settings.targetServer = server
  1898 +settings.store() # persist the settings
  1899 +
  1900 +# run code that gets a new value for userId, and persist the settings
  1901 +user = "biden_joe"
  1902 +settings.userId = user
  1903 +settings.store()
  1904 +
  1905 +
  1906 +# Example C
  1907 +#-----------------------------------------------------------------------
  1908 +# recover the Settings instance, change an attribute, and store it again.
  1909 +#-----------------------------------------------------------------------
  1910 +settings = Settings(settingsFile)
  1911 +settings.userId = "vanrossum_g"
  1912 +settings.store()
  1913 +
  1914 +"""
  1915 + def __init__(self, filename): # obtaining filename is required
  1916 + self.filename = None
  1917 + raise NotImplementedError()
  1918 +
  1919 + def restore(self):
  1920 + """
  1921 + Set the values of whatever attributes are recoverable
  1922 + from the pickle file.
  1923 +
  1924 + Populate the attributes (the __dict__) of the EgStore object
  1925 + from the attributes (the __dict__) of the pickled object.
  1926 +
  1927 + If the pickled object has attributes that have been initialized
  1928 + in the EgStore object, then those attributes of the EgStore object
  1929 + will be replaced by the values of the corresponding attributes
  1930 + in the pickled object.
  1931 +
  1932 + If the pickled object is missing some attributes that have
  1933 + been initialized in the EgStore object, then those attributes
  1934 + of the EgStore object will retain the values that they were
  1935 + initialized with.
  1936 +
  1937 + If the pickled object has some attributes that were not
  1938 + initialized in the EgStore object, then those attributes
  1939 + will be ignored.
  1940 +
  1941 + IN SUMMARY:
  1942 +
  1943 + After the recover() operation, the EgStore object will have all,
  1944 + and only, the attributes that it had when it was initialized.
  1945 +
  1946 + Where possible, those attributes will have values recovered
  1947 + from the pickled object.
  1948 + """
  1949 + if not os.path.exists(self.filename): return self
  1950 + if not os.path.isfile(self.filename): return self
  1951 +
  1952 + try:
  1953 + f = open(self.filename,"rb")
  1954 + unpickledObject = pickle.load(f)
  1955 + f.close()
  1956 +
  1957 + for key in list(self.__dict__.keys()):
  1958 + default = self.__dict__[key]
  1959 + self.__dict__[key] = unpickledObject.__dict__.get(key,default)
  1960 + except:
  1961 + pass
  1962 +
  1963 + return self
  1964 +
  1965 + def store(self):
  1966 + """
  1967 + Save the attributes of the EgStore object to a pickle file.
  1968 + Note that if the directory for the pickle file does not already exist,
  1969 + the store operation will fail.
  1970 + """
  1971 + f = open(self.filename, "wb")
  1972 + pickle.dump(self, f)
  1973 + f.close()
  1974 +
  1975 +
  1976 + def kill(self):
  1977 + """
  1978 + Delete my persistent file (i.e. pickle file), if it exists.
  1979 + """
  1980 + if os.path.isfile(self.filename):
  1981 + os.remove(self.filename)
  1982 + return
  1983 +
  1984 + def __str__(self):
  1985 + """
  1986 + return my contents as a string in an easy-to-read format.
  1987 + """
  1988 + # find the length of the longest attribute name
  1989 + longest_key_length = 0
  1990 + keys = []
  1991 + for key in self.__dict__.keys():
  1992 + keys.append(key)
  1993 + longest_key_length = max(longest_key_length, len(key))
  1994 +
  1995 + keys.sort() # sort the attribute names
  1996 + lines = []
  1997 + for key in keys:
  1998 + value = self.__dict__[key]
  1999 + key = key.ljust(longest_key_length)
  2000 + lines.append("%s : %s\n" % (key,repr(value)) )
  2001 + return "".join(lines) # return a string showing the attributes
  2002 +
  2003 +
  2004 +
  2005 +
  2006 +#-----------------------------------------------------------------------
  2007 +#
  2008 +# test/demo easygui
  2009 +#
  2010 +#-----------------------------------------------------------------------
  2011 +def egdemo():
  2012 + """
  2013 + Run the EasyGui demo.
  2014 + """
  2015 + # clear the console
  2016 + writeln("\n" * 100)
  2017 +
  2018 + intro_message = ("Pick the kind of box that you wish to demo.\n"
  2019 + + "\n * Python version " + sys.version
  2020 + + "\n * EasyGui version " + egversion
  2021 + + "\n * Tk version " + str(TkVersion)
  2022 + )
  2023 +
  2024 + #========================================== END DEMONSTRATION DATA
  2025 +
  2026 +
  2027 + while 1: # do forever
  2028 + choices = [
  2029 + "msgbox",
  2030 + "buttonbox",
  2031 + "buttonbox(image) -- a buttonbox that displays an image",
  2032 + "choicebox",
  2033 + "multchoicebox",
  2034 + "textbox",
  2035 + "ynbox",
  2036 + "ccbox",
  2037 + "enterbox",
  2038 + "enterbox(image) -- an enterbox that displays an image",
  2039 + "exceptionbox",
  2040 + "codebox",
  2041 + "integerbox",
  2042 + "boolbox",
  2043 + "indexbox",
  2044 + "filesavebox",
  2045 + "fileopenbox",
  2046 + "passwordbox",
  2047 + "multenterbox",
  2048 + "multpasswordbox",
  2049 + "diropenbox",
  2050 + "About EasyGui",
  2051 + " Help"
  2052 + ]
  2053 + choice = choicebox(msg=intro_message
  2054 + , title="EasyGui " + egversion
  2055 + , choices=choices)
  2056 +
  2057 + if not choice: return
  2058 +
  2059 + reply = choice.split()
  2060 +
  2061 + if reply[0] == "msgbox":
  2062 + reply = msgbox("short msg", "This is a long title")
  2063 + writeln("Reply was: %s" % repr(reply))
  2064 +
  2065 + elif reply[0] == "About":
  2066 + reply = abouteasygui()
  2067 +
  2068 + elif reply[0] == "Help":
  2069 + _demo_help()
  2070 +
  2071 + elif reply[0] == "buttonbox":
  2072 + reply = buttonbox()
  2073 + writeln("Reply was: %s" % repr(reply))
  2074 +
  2075 + title = "Demo of Buttonbox with many, many buttons!"
  2076 + msg = "This buttonbox shows what happens when you specify too many buttons."
  2077 + reply = buttonbox(msg=msg, title=title, choices=choices)
  2078 + writeln("Reply was: %s" % repr(reply))
  2079 +
  2080 + elif reply[0] == "buttonbox(image)":
  2081 + _demo_buttonbox_with_image()
  2082 +
  2083 + elif reply[0] == "boolbox":
  2084 + reply = boolbox()
  2085 + writeln("Reply was: %s" % repr(reply))
  2086 +
  2087 + elif reply[0] == "enterbox":
  2088 + image = "python_and_check_logo.gif"
  2089 + message = "Enter the name of your best friend."\
  2090 + "\n(Result will be stripped.)"
  2091 + reply = enterbox(message, "Love!", " Suzy Smith ")
  2092 + writeln("Reply was: %s" % repr(reply))
  2093 +
  2094 + message = "Enter the name of your best friend."\
  2095 + "\n(Result will NOT be stripped.)"
  2096 + reply = enterbox(message, "Love!", " Suzy Smith ",strip=False)
  2097 + writeln("Reply was: %s" % repr(reply))
  2098 +
  2099 + reply = enterbox("Enter the name of your worst enemy:", "Hate!")
  2100 + writeln("Reply was: %s" % repr(reply))
  2101 +
  2102 + elif reply[0] == "enterbox(image)":
  2103 + image = "python_and_check_logo.gif"
  2104 + message = "What kind of snake is this?"
  2105 + reply = enterbox(message, "Quiz",image=image)
  2106 + writeln("Reply was: %s" % repr(reply))
  2107 +
  2108 + elif reply[0] == "exceptionbox":
  2109 + try:
  2110 + thisWillCauseADivideByZeroException = 1/0
  2111 + except:
  2112 + exceptionbox()
  2113 +
  2114 + elif reply[0] == "integerbox":
  2115 + reply = integerbox(
  2116 + "Enter a number between 3 and 333",
  2117 + "Demo: integerbox WITH a default value",
  2118 + 222, 3, 333)
  2119 + writeln("Reply was: %s" % repr(reply))
  2120 +
  2121 + reply = integerbox(
  2122 + "Enter a number between 0 and 99",
  2123 + "Demo: integerbox WITHOUT a default value"
  2124 + )
  2125 + writeln("Reply was: %s" % repr(reply))
  2126 +
  2127 + elif reply[0] == "diropenbox" : _demo_diropenbox()
  2128 + elif reply[0] == "fileopenbox": _demo_fileopenbox()
  2129 + elif reply[0] == "filesavebox": _demo_filesavebox()
  2130 +
  2131 + elif reply[0] == "indexbox":
  2132 + title = reply[0]
  2133 + msg = "Demo of " + reply[0]
  2134 + choices = ["Choice1", "Choice2", "Choice3", "Choice4"]
  2135 + reply = indexbox(msg, title, choices)
  2136 + writeln("Reply was: %s" % repr(reply))
  2137 +
  2138 + elif reply[0] == "passwordbox":
  2139 + reply = passwordbox("Demo of password box WITHOUT default"
  2140 + + "\n\nEnter your secret password", "Member Logon")
  2141 + writeln("Reply was: %s" % str(reply))
  2142 +
  2143 + reply = passwordbox("Demo of password box WITH default"
  2144 + + "\n\nEnter your secret password", "Member Logon", "alfie")
  2145 + writeln("Reply was: %s" % str(reply))
  2146 +
  2147 + elif reply[0] == "multenterbox":
  2148 + msg = "Enter your personal information"
  2149 + title = "Credit Card Application"
  2150 + fieldNames = ["Name","Street Address","City","State","ZipCode"]
  2151 + fieldValues = [] # we start with blanks for the values
  2152 + fieldValues = multenterbox(msg,title, fieldNames)
  2153 +
  2154 + # make sure that none of the fields was left blank
  2155 + while 1:
  2156 + if fieldValues == None: break
  2157 + errmsg = ""
  2158 + for i in range(len(fieldNames)):
  2159 + if fieldValues[i].strip() == "":
  2160 + errmsg = errmsg + ('"%s" is a required field.\n\n' % fieldNames[i])
  2161 + if errmsg == "": break # no problems found
  2162 + fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues)
  2163 +
  2164 + writeln("Reply was: %s" % str(fieldValues))
  2165 +
  2166 + elif reply[0] == "multpasswordbox":
  2167 + msg = "Enter logon information"
  2168 + title = "Demo of multpasswordbox"
  2169 + fieldNames = ["Server ID", "User ID", "Password"]
  2170 + fieldValues = [] # we start with blanks for the values
  2171 + fieldValues = multpasswordbox(msg,title, fieldNames)
  2172 +
  2173 + # make sure that none of the fields was left blank
  2174 + while 1:
  2175 + if fieldValues == None: break
  2176 + errmsg = ""
  2177 + for i in range(len(fieldNames)):
  2178 + if fieldValues[i].strip() == "":
  2179 + errmsg = errmsg + ('"%s" is a required field.\n\n' % fieldNames[i])
  2180 + if errmsg == "": break # no problems found
  2181 + fieldValues = multpasswordbox(errmsg, title, fieldNames, fieldValues)
  2182 +
  2183 + writeln("Reply was: %s" % str(fieldValues))
  2184 +
  2185 + elif reply[0] == "ynbox":
  2186 + title = "Demo of ynbox"
  2187 + msg = "Were you expecting the Spanish Inquisition?"
  2188 + reply = ynbox(msg, title)
  2189 + writeln("Reply was: %s" % repr(reply))
  2190 + if reply:
  2191 + msgbox("NOBODY expects the Spanish Inquisition!", "Wrong!")
  2192 +
  2193 + elif reply[0] == "ccbox":
  2194 + title = "Demo of ccbox"
  2195 + reply = ccbox(msg,title)
  2196 + writeln("Reply was: %s" % repr(reply))
  2197 +
  2198 + elif reply[0] == "choicebox":
  2199 + title = "Demo of choicebox"
  2200 + longchoice = "This is an example of a very long option which you may or may not wish to choose."*2
  2201 + listChoices = ["nnn", "ddd", "eee", "fff", "aaa", longchoice
  2202 + , "aaa", "bbb", "ccc", "ggg", "hhh", "iii", "jjj", "kkk", "LLL", "mmm" , "nnn", "ooo", "ppp", "qqq", "rrr", "sss", "ttt", "uuu", "vvv"]
  2203 +
  2204 + msg = "Pick something. " + ("A wrapable sentence of text ?! "*30) + "\nA separate line of text."*6
  2205 + reply = choicebox(msg=msg, choices=listChoices)
  2206 + writeln("Reply was: %s" % repr(reply))
  2207 +
  2208 + msg = "Pick something. "
  2209 + reply = choicebox(msg=msg, title=title, choices=listChoices)
  2210 + writeln("Reply was: %s" % repr(reply))
  2211 +
  2212 + msg = "Pick something. "
  2213 + reply = choicebox(msg="The list of choices is empty!", choices=[])
  2214 + writeln("Reply was: %s" % repr(reply))
  2215 +
  2216 + elif reply[0] == "multchoicebox":
  2217 + listChoices = ["aaa", "bbb", "ccc", "ggg", "hhh", "iii", "jjj", "kkk"
  2218 + , "LLL", "mmm" , "nnn", "ooo", "ppp", "qqq"
  2219 + , "rrr", "sss", "ttt", "uuu", "vvv"]
  2220 +
  2221 + msg = "Pick as many choices as you wish."
  2222 + reply = multchoicebox(msg,"Demo of multchoicebox", listChoices)
  2223 + writeln("Reply was: %s" % repr(reply))
  2224 +
  2225 + elif reply[0] == "textbox": _demo_textbox(reply[0])
  2226 + elif reply[0] == "codebox": _demo_codebox(reply[0])
  2227 +
  2228 + else:
  2229 + msgbox("Choice\n\n" + choice + "\n\nis not recognized", "Program Logic Error")
  2230 + return
  2231 +
  2232 +
  2233 +def _demo_textbox(reply):
  2234 + text_snippet = ((\
  2235 +"""It was the best of times, and it was the worst of times. The rich ate cake, and the poor had cake recommended to them, but wished only for enough cash to buy bread. The time was ripe for revolution! """ \
  2236 +*5)+"\n\n")*10
  2237 + title = "Demo of textbox"
  2238 + msg = "Here is some sample text. " * 16
  2239 + reply = textbox(msg, title, text_snippet)
  2240 + writeln("Reply was: %s" % str(reply))
  2241 +
  2242 +def _demo_codebox(reply):
  2243 + code_snippet = ("dafsdfa dasflkj pp[oadsij asdfp;ij asdfpjkop asdfpok asdfpok asdfpok"*3) +"\n"+\
  2244 +"""# here is some dummy Python code
  2245 +for someItem in myListOfStuff:
  2246 + do something(someItem)
  2247 + do something()
  2248 + do something()
  2249 + if somethingElse(someItem):
  2250 + doSomethingEvenMoreInteresting()
  2251 +
  2252 +"""*16
  2253 + msg = "Here is some sample code. " * 16
  2254 + reply = codebox(msg, "Code Sample", code_snippet)
  2255 + writeln("Reply was: %s" % repr(reply))
  2256 +
  2257 +
  2258 +def _demo_buttonbox_with_image():
  2259 +
  2260 + msg = "Do you like this picture?\nIt is "
  2261 + choices = ["Yes","No","No opinion"]
  2262 +
  2263 + for image in [
  2264 + "python_and_check_logo.gif"
  2265 + ,"python_and_check_logo.jpg"
  2266 + ,"python_and_check_logo.png"
  2267 + ,"zzzzz.gif"]:
  2268 +
  2269 + reply=buttonbox(msg + image,image=image,choices=choices)
  2270 + writeln("Reply was: %s" % repr(reply))
  2271 +
  2272 +
  2273 +def _demo_help():
  2274 + savedStdout = sys.stdout # save the sys.stdout file object
  2275 + sys.stdout = capturedOutput = StringIO()
  2276 + help("easygui")
  2277 + sys.stdout = savedStdout # restore the sys.stdout file object
  2278 + codebox("EasyGui Help",text=capturedOutput.getvalue())
  2279 +
  2280 +def _demo_filesavebox():
  2281 + filename = "myNewFile.txt"
  2282 + title = "File SaveAs"
  2283 + msg ="Save file as:"
  2284 +
  2285 + f = filesavebox(msg,title,default=filename)
  2286 + writeln("You chose to save file: %s" % f)
  2287 +
  2288 +def _demo_diropenbox():
  2289 + title = "Demo of diropenbox"
  2290 + msg = "Pick the directory that you wish to open."
  2291 + d = diropenbox(msg, title)
  2292 + writeln("You chose directory...: %s" % d)
  2293 +
  2294 + d = diropenbox(msg, title,default="./")
  2295 + writeln("You chose directory...: %s" % d)
  2296 +
  2297 + d = diropenbox(msg, title,default="c:/")
  2298 + writeln("You chose directory...: %s" % d)
  2299 +
  2300 +
  2301 +def _demo_fileopenbox():
  2302 + msg = "Python files"
  2303 + title = "Open files"
  2304 + default="*.py"
  2305 + f = fileopenbox(msg,title,default=default)
  2306 + writeln("You chose to open file: %s" % f)
  2307 +
  2308 + default="./*.gif"
  2309 + filetypes = ["*.jpg",["*.zip","*.tgs","*.gz", "Archive files"],["*.htm", "*.html","HTML files"]]
  2310 + f = fileopenbox(msg,title,default=default,filetypes=filetypes)
  2311 + writeln("You chose to open file: %s" % f)
  2312 +
  2313 + """#deadcode -- testing ----------------------------------------
  2314 + f = fileopenbox(None,None,default=default)
  2315 + writeln("You chose to open file: %s" % f)
  2316 +
  2317 + f = fileopenbox(None,title,default=default)
  2318 + writeln("You chose to open file: %s" % f)
  2319 +
  2320 + f = fileopenbox(msg,None,default=default)
  2321 + writeln("You chose to open file: %s" % f)
  2322 +
  2323 + f = fileopenbox(default=default)
  2324 + writeln("You chose to open file: %s" % f)
  2325 +
  2326 + f = fileopenbox(default=None)
  2327 + writeln("You chose to open file: %s" % f)
  2328 + #----------------------------------------------------deadcode """
  2329 +
  2330 +
  2331 +def _dummy():
  2332 + pass
  2333 +
  2334 +EASYGUI_ABOUT_INFORMATION = '''
  2335 +========================================================================
  2336 +0.96(2010-08-29)
  2337 +========================================================================
  2338 +This version fixes some problems with version independence.
  2339 +
  2340 +BUG FIXES
  2341 +------------------------------------------------------
  2342 + * A statement with Python 2.x-style exception-handling syntax raised
  2343 + a syntax error when running under Python 3.x.
  2344 + Thanks to David Williams for reporting this problem.
  2345 +
  2346 + * Under some circumstances, PIL was unable to display non-gif images
  2347 + that it should have been able to display.
  2348 + The cause appears to be non-version-independent import syntax.
  2349 + PIL modules are now imported with a version-independent syntax.
  2350 + Thanks to Horst Jens for reporting this problem.
  2351 +
  2352 +LICENSE CHANGE
  2353 +------------------------------------------------------
  2354 +Starting with this version, EasyGui is licensed under what is generally known as
  2355 +the "modified BSD license" (aka "revised BSD", "new BSD", "3-clause BSD").
  2356 +This license is GPL-compatible but less restrictive than GPL.
  2357 +Earlier versions were licensed under the Creative Commons Attribution License 2.0.
  2358 +
  2359 +
  2360 +========================================================================
  2361 +0.95(2010-06-12)
  2362 +========================================================================
  2363 +
  2364 +ENHANCEMENTS
  2365 +------------------------------------------------------
  2366 + * Previous versions of EasyGui could display only .gif image files using the
  2367 + msgbox "image" argument. This version can now display all image-file formats
  2368 + supported by PIL the Python Imaging Library) if PIL is installed.
  2369 + If msgbox is asked to open a non-gif image file, it attempts to import
  2370 + PIL and to use PIL to convert the image file to a displayable format.
  2371 + If PIL cannot be imported (probably because PIL is not installed)
  2372 + EasyGui displays an error message saying that PIL must be installed in order
  2373 + to display the image file.
  2374 +
  2375 + Note that
  2376 + http://www.pythonware.com/products/pil/
  2377 + says that PIL doesn't yet support Python 3.x.
  2378 +
  2379 +
  2380 +========================================================================
  2381 +0.94(2010-06-06)
  2382 +========================================================================
  2383 +
  2384 +ENHANCEMENTS
  2385 +------------------------------------------------------
  2386 + * The codebox and textbox functions now return the contents of the box, rather
  2387 + than simply the name of the button ("Yes"). This makes it possible to use
  2388 + codebox and textbox as data-entry widgets. A big "thank you!" to Dominic
  2389 + Comtois for requesting this feature, patiently explaining his requirement,
  2390 + and helping to discover the tkinter techniques to implement it.
  2391 +
  2392 + NOTE THAT in theory this change breaks backward compatibility. But because
  2393 + (in previous versions of EasyGui) the value returned by codebox and textbox
  2394 + was meaningless, no application should have been checking it. So in actual
  2395 + practice, this change should not break backward compatibility.
  2396 +
  2397 + * Added support for SPACEBAR to command buttons. Now, when keyboard
  2398 + focus is on a command button, a press of the SPACEBAR will act like
  2399 + a press of the ENTER key; it will activate the command button.
  2400 +
  2401 + * Added support for keyboard navigation with the arrow keys (up,down,left,right)
  2402 + to the fields and buttons in enterbox, multenterbox and multpasswordbox,
  2403 + and to the buttons in choicebox and all buttonboxes.
  2404 +
  2405 + * added highlightthickness=2 to entry fields in multenterbox and
  2406 + multpasswordbox. Now it is easier to tell which entry field has
  2407 + keyboard focus.
  2408 +
  2409 +
  2410 +BUG FIXES
  2411 +------------------------------------------------------
  2412 + * In EgStore, the pickle file is now opened with "rb" and "wb" rather than
  2413 + with "r" and "w". This change is necessary for compatibility with Python 3+.
  2414 + Thanks to Marshall Mattingly for reporting this problem and providing the fix.
  2415 +
  2416 + * In integerbox, the actual argument names did not match the names described
  2417 + in the docstring. Thanks to Daniel Zingaro of at University of Toronto for
  2418 + reporting this problem.
  2419 +
  2420 + * In integerbox, the "argLowerBound" and "argUpperBound" arguments have been
  2421 + renamed to "lowerbound" and "upperbound" and the docstring has been corrected.
  2422 +
  2423 + NOTE THAT THIS CHANGE TO THE ARGUMENT-NAMES BREAKS BACKWARD COMPATIBILITY.
  2424 + If argLowerBound or argUpperBound are used, an AssertionError with an
  2425 + explanatory error message is raised.
  2426 +
  2427 + * In choicebox, the signature to choicebox incorrectly showed choicebox as
  2428 + accepting a "buttons" argument. The signature has been fixed.
  2429 +
  2430 +
  2431 +========================================================================
  2432 +0.93(2009-07-07)
  2433 +========================================================================
  2434 +
  2435 +ENHANCEMENTS
  2436 +------------------------------------------------------
  2437 +
  2438 + * Added exceptionbox to display stack trace of exceptions
  2439 +
  2440 + * modified names of some font-related constants to make it
  2441 + easier to customize them
  2442 +
  2443 +
  2444 +========================================================================
  2445 +0.92(2009-06-22)
  2446 +========================================================================
  2447 +
  2448 +ENHANCEMENTS
  2449 +------------------------------------------------------
  2450 +
  2451 + * Added EgStore class to to provide basic easy-to-use persistence.
  2452 +
  2453 +BUG FIXES
  2454 +------------------------------------------------------
  2455 +
  2456 + * Fixed a bug that was preventing Linux users from copying text out of
  2457 + a textbox and a codebox. This was not a problem for Windows users.
  2458 +
  2459 +'''
  2460 +
  2461 +def abouteasygui():
  2462 + """
  2463 + shows the easygui revision history
  2464 + """
  2465 + codebox("About EasyGui\n"+egversion,"EasyGui",EASYGUI_ABOUT_INFORMATION)
  2466 + return None
  2467 +
  2468 +
  2469 +
  2470 +if __name__ == '__main__':
  2471 + if True:
  2472 + egdemo()
  2473 + else:
  2474 + # test the new root feature
  2475 + root = Tk()
  2476 + msg = """This is a test of a main Tk() window in which we will place an easygui msgbox.
  2477 + It will be an interesting experiment.\n\n"""
  2478 + messageWidget = Message(root, text=msg, width=1000)
  2479 + messageWidget.pack(side=TOP, expand=YES, fill=X, padx='3m', pady='3m')
  2480 + messageWidget = Message(root, text=msg, width=1000)
  2481 + messageWidget.pack(side=TOP, expand=YES, fill=X, padx='3m', pady='3m')
  2482 +
  2483 +
  2484 + msgbox("this is a test of passing in boxRoot", root=root)
  2485 + msgbox("this is a second test of passing in boxRoot", root=root)
  2486 +
  2487 + reply = enterbox("Enter something", root=root)
  2488 + writeln("You wrote:", reply)
  2489 +
  2490 + reply = enterbox("Enter something else", root=root)
  2491 + writeln("You wrote:", reply)
  2492 + root.destroy()
... ...
oletools/thirdparty/xxxswf/LICENSE.txt 0 → 100644
  1 +++ a/oletools/thirdparty/xxxswf/LICENSE.txt
  1 +xxxswf.py is published by Alexander Hanel on
  2 +http://hooked-on-mnemonics.blogspot.nl/2011/12/xxxswfpy.html
  3 +without explicit license.
0 4 \ No newline at end of file
... ...
oletools/thirdparty/xxxswf/__init__.py 0 → 100644
  1 +++ a/oletools/thirdparty/xxxswf/__init__.py
... ...
oletools/thirdparty/xxxswf/xxxswf.py 0 → 100644
  1 +++ a/oletools/thirdparty/xxxswf/xxxswf.py
  1 +# xxxswf.py was created by alexander dot hanel at gmail dot com
  2 +# version 0.1
  3 +# Date - 12-07-2011
  4 +# To do list
  5 +# - Tag Parser
  6 +# - ActionScript Decompiler
  7 +
  8 +import fnmatch
  9 +import hashlib
  10 +import imp
  11 +import math
  12 +import os
  13 +import re
  14 +import struct
  15 +import sys
  16 +import time
  17 +from StringIO import StringIO
  18 +from optparse import OptionParser
  19 +import zlib
  20 +
  21 +def checkMD5(md5):
  22 +# checks if MD5 has been seen in MD5 Dictionary
  23 +# MD5Dict contains the MD5 and the CVE
  24 +# For { 'MD5':'CVE', 'MD5-1':'CVE-1', 'MD5-2':'CVE-2'}
  25 + MD5Dict = {'c46299a5015c6d31ad5766cb49e4ab4b':'CVE-XXXX-XXXX'}
  26 + if MD5Dict.get(md5):
  27 + print '\t[BAD] MD5 Match on', MD5Dict.get(md5)
  28 + return
  29 +
  30 +def bad(f):
  31 + for idx, x in enumerate(findSWF(f)):
  32 + tmp = verifySWF(f,x)
  33 + if tmp != None:
  34 + yaraScan(tmp)
  35 + checkMD5(hashBuff(tmp))
  36 + return
  37 +
  38 +def yaraScan(d):
  39 +# d = buffer of the read file
  40 +# Scans SWF using Yara
  41 + # test if yara module is installed
  42 + # if not Yara can be downloaded from http://code.google.com/p/yara-project/
  43 + try:
  44 + imp.find_module('yara')
  45 + import yara
  46 + except ImportError:
  47 + print '\t[ERROR] Yara module not installed - aborting scan'
  48 + return
  49 + # test for yara compile errors
  50 + try:
  51 + r = yara.compile(r'rules.yar')
  52 + except:
  53 + pass
  54 + print '\t[ERROR] Yara compile error - aborting scan'
  55 + return
  56 + # get matches
  57 + m = r.match(data=d)
  58 + # print matches
  59 + for X in m:
  60 + print '\t[BAD] Yara Signature Hit:', X
  61 + return
  62 +
  63 +def findSWF(d):
  64 +# d = buffer of the read file
  65 +# Search for SWF Header Sigs in files
  66 + return [tmp.start() for tmp in re.finditer('CWS|FWS', d.read())]
  67 +
  68 +def hashBuff(d):
  69 +# d = buffer of the read file
  70 +# This function hashes the buffer
  71 +# source: http://stackoverflow.com/q/5853830
  72 + if type(d) is str:
  73 + d = StringIO(d)
  74 + md5 = hashlib.md5()
  75 + while True:
  76 + data = d.read(128)
  77 + if not data:
  78 + break
  79 + md5.update(data)
  80 + return md5.hexdigest()
  81 +
  82 +def verifySWF(f,addr):
  83 + # Start of SWF
  84 + f.seek(addr)
  85 + # Read Header
  86 + header = f.read(3)
  87 + # Read Version
  88 + ver = struct.unpack('<b', f.read(1))[0]
  89 + # Read SWF Size
  90 + size = struct.unpack('<i', f.read(4))[0]
  91 + # Start of SWF
  92 + f.seek(addr)
  93 + try:
  94 + # Read SWF into buffer. If compressed read uncompressed size.
  95 + t = f.read(size)
  96 + except:
  97 + pass
  98 + # Error check for invalid SWF
  99 + print ' - [ERROR] Invalid SWF Size'
  100 + return None
  101 + if type(t) is str:
  102 + f = StringIO(t)
  103 + # Error check for version above 20
  104 + if ver > 20:
  105 + print ' - [ERROR] Invalid SWF Version'
  106 + return None
  107 +
  108 + if 'CWS' in header:
  109 + try:
  110 + f.read(3)
  111 + tmp = 'FWS' + f.read(5) + zlib.decompress(f.read())
  112 + print ' - CWS Header'
  113 + return tmp
  114 +
  115 + except:
  116 + pass
  117 + print '- [ERROR]: Zlib decompression error. Invalid CWS SWF'
  118 + return None
  119 +
  120 + elif 'FWS' in header:
  121 + try:
  122 + tmp = f.read(size)
  123 + print ' - FWS Header'
  124 + return tmp
  125 +
  126 + except:
  127 + pass
  128 + print ' - [ERROR] Invalid SWF Size'
  129 + return None
  130 +
  131 + else:
  132 + print ' - [Error] Logic Error Blame Programmer'
  133 + return None
  134 +
  135 +def headerInfo(f):
  136 +# f is the already opended file handle
  137 +# Yes, the format is is a rip off SWFDump. Can you blame me? Their tool is awesome.
  138 + # SWFDump FORMAT
  139 + # [HEADER] File version: 8
  140 + # [HEADER] File is zlib compressed. Ratio: 52%
  141 + # [HEADER] File size: 37536
  142 + # [HEADER] Frame rate: 18.000000
  143 + # [HEADER] Frame count: 323
  144 + # [HEADER] Movie width: 217.00
  145 + # [HEADER] Movie height: 85.00
  146 + if type(f) is str:
  147 + f = StringIO(f)
  148 + sig = f.read(3)
  149 + print '\t[HEADER] File header:', sig
  150 + if 'C' in sig:
  151 + print '\t[HEADER] File is zlib compressed.'
  152 + version = struct.unpack('<b', f.read(1))[0]
  153 + print '\t[HEADER] File version:', version
  154 + size = struct.unpack('<i', f.read(4))[0]
  155 + print '\t[HEADER] File size:', size
  156 + # deflate compressed SWF
  157 + if 'C' in sig:
  158 + f = verifySWF(f,0)
  159 + if type(f) is str:
  160 + f = StringIO(f)
  161 + f.seek(0, 0)
  162 + x = f.read(8)
  163 + ta = f.tell()
  164 + tmp = struct.unpack('<b', f.read(1))[0]
  165 + nbit = tmp >> 3
  166 + print '\t[HEADER] Rect Nbit:', nbit
  167 + # Curretely the nbit is static at 15. This could be modified in the
  168 + # future. If larger than 9 this will break the struct unpack. Will have
  169 + # to revist must be a more effective way to deal with bits. Tried to keep
  170 + # the algo but damn this is ugly...
  171 + f.seek(ta)
  172 + rect = struct.unpack('>Q', f.read(int(math.ceil((nbit*4)/8.0))))[0]
  173 + tmp = struct.unpack('<b', f.read(1))[0]
  174 + tmp = bin(tmp>>7)[2:].zfill(1)
  175 + # bin requires Python 2.6 or higher
  176 + # skips string '0b' and the nbit
  177 + rect = bin(rect)[7:]
  178 + xmin = int(rect[0:nbit-1],2)
  179 + print '\t[HEADER] Rect Xmin:', xmin
  180 + xmax = int(rect[nbit:(nbit*2)-1],2)
  181 + print '\t[HEADER] Rect Xmax:', xmax
  182 + ymin = int(rect[nbit*2:(nbit*3)-1],2)
  183 + print '\t[HEADER] Rect Ymin:', ymin
  184 + # one bit needs to be added, my math might be off here
  185 + ymax = int(rect[nbit*3:(nbit*4)-1] + str(tmp) ,2)
  186 + print '\t[HEADER] Rect Ymax:', ymax
  187 + framerate = struct.unpack('<H', f.read(2))[0]
  188 + print '\t[HEADER] Frame Rate:', framerate
  189 + framecount = struct.unpack('<H', f.read(2))[0]
  190 + print '\t[HEADER] Frame Count:', framecount
  191 +
  192 +def walk4SWF(path):
  193 + # returns a list of [folder-path, [addr1,addrw2]]
  194 + # Don't ask, will come back to this code.
  195 + p = ['',[]]
  196 + r = p*0
  197 + if os.path.isdir(path) != True and path != '':
  198 + print '\t[ERROR] walk4SWF path must be a dir.'
  199 + return
  200 + for root, dirs, files in os.walk(path):
  201 + for name in files:
  202 + try:
  203 + x = open(os.path.join(root, name), 'rb')
  204 + except:
  205 + pass
  206 + break
  207 + y = findSWF(x)
  208 + if len(y) != 0:
  209 + # Path of file SWF
  210 + p[0] = os.path.join(root, name)
  211 + # contains list of the file offset of SWF header
  212 + p[1] = y
  213 + r.insert(len(r),p)
  214 + p = ['',[]]
  215 + y = ''
  216 + x.close()
  217 + return r
  218 +
  219 +def tagsInfo(f):
  220 + return
  221 +
  222 +def fileExist(n, ext):
  223 + # Checks the working dir to see if the file is
  224 + # already in the dir. If exists the file will
  225 + # be named name.count.ext (n.c.ext). No more than
  226 + # 50 matching MD5s will be written to the dir.
  227 + if os.path.exists( n + '.' + ext):
  228 + c = 2
  229 + while os.path.exists(n + '.' + str(c) + '.' + ext):
  230 + c = c + 1
  231 + if c == 50:
  232 + print '\t[ERROR] Skipped 50 Matching MD5 SWFs'
  233 + break
  234 + n = n + '.' + str(c)
  235 +
  236 + return n + '.' + ext
  237 +
  238 +def CWSize(f):
  239 + # The file size in the header is of the uncompressed SWF.
  240 + # To estimate the size of the compressed data, we can grab
  241 + # the length, read that amount, deflate the data, then
  242 + # compress the data again, and then call len(). This will
  243 + # give us the length of the compressed SWF.
  244 + return
  245 +
  246 +def compressSWF(f):
  247 + if type(f) is str:
  248 + f = StringIO(f)
  249 + try:
  250 + f.read(3)
  251 + tmp = 'CWS' + f.read(5) + zlib.compress(f.read())
  252 + return tmp
  253 + except:
  254 + pass
  255 + print '\t[ERROR] SWF Zlib Compression Failed'
  256 + return None
  257 +
  258 +def disneyland(f,filename, options):
  259 + # because this is where the magic happens
  260 + # but seriously I did the recursion part last..
  261 + retfindSWF = findSWF(f)
  262 + f.seek(0)
  263 + print '\n[SUMMARY] %d SWF(s) in MD5:%s:%s' % ( len(retfindSWF),hashBuff(f), filename )
  264 + # for each SWF in file
  265 + for idx, x in enumerate(retfindSWF):
  266 + print '\t[ADDR] SWF %d at %s' % (idx+1, hex(x)),
  267 + f.seek(x)
  268 + h = f.read(1)
  269 + f.seek(x)
  270 + swf = verifySWF(f,x)
  271 + if swf == None:
  272 + continue
  273 + if options.extract != None:
  274 + name = fileExist(hashBuff(swf), 'swf')
  275 + print '\t\t[FILE] Carved SWF MD5: %s' % name
  276 + try:
  277 + o = open(name, 'wb+')
  278 + except IOError, e:
  279 + print '\t[ERROR] Could Not Create %s ' % e
  280 + continue
  281 + o.write(swf)
  282 + o.close()
  283 + if options.yara != None:
  284 + yaraScan(swf)
  285 + if options.md5scan != None:
  286 + checkMD5(hashBuff(swf))
  287 + if options.decompress != None:
  288 + name = fileExist(hashBuff(swf), 'swf')
  289 + print '\t\t[FILE] Carved SWF MD5: %s' % name
  290 + try:
  291 + o = open(name, 'wb+')
  292 + except IOError, e:
  293 + print '\t[ERROR] Could Not Create %s ' % e
  294 + continue
  295 + o.write(swf)
  296 + o.close()
  297 + if options.header != None:
  298 + headerInfo(swf)
  299 + if options.compress != None:
  300 + swf = compressSWF(swf)
  301 + if swf == None:
  302 + continue
  303 + name = fileExist(hashBuff(swf), 'swf')
  304 + print '\t\t[FILE] Compressed SWF MD5: %s' % name
  305 + try:
  306 + o = open(name, 'wb+')
  307 + except IOError, e:
  308 + print '\t[ERROR] Could Not Create %s ' % e
  309 + continue
  310 + o.write(swf)
  311 + o.close()
  312 +
  313 +def main():
  314 + # Scenarios:
  315 + # Scan file for SWF(s)
  316 + # Scan file for SWF(s) and extract them
  317 + # Scan file for SWF(s) and scan them with Yara
  318 + # Scan file for SWF(s), extract them and scan with Yara
  319 + # Scan directory recursively for files that contain SWF(s)
  320 + # Scan directory recursively for files that contain SWF(s) and extract them
  321 +
  322 + parser = OptionParser()
  323 + usage = 'usage: %prog [options] <file.bad>'
  324 + parser = OptionParser(usage=usage)
  325 + parser.add_option('-x', '--extract', action='store_true', dest='extract', help='Extracts the embedded SWF(s), names it MD5HASH.swf & saves it in the working dir. No addition args needed')
  326 + parser.add_option('-y', '--yara', action='store_true', dest='yara', help='Scans the SWF(s) with yara. If the SWF(s) is compressed it will be deflated. No addition args needed')
  327 + parser.add_option('-s', '--md5scan', action='store_true', dest='md5scan', help='Scans the SWF(s) for MD5 signatures. Please see func checkMD5 to define hashes. No addition args needed')
  328 + parser.add_option('-H', '--header', action='store_true', dest='header', help='Displays the SWFs file header. No addition args needed')
  329 + parser.add_option('-d', '--decompress', action='store_true', dest='decompress', help='Deflates compressed SWFS(s)')
  330 + parser.add_option('-r', '--recdir', dest='PATH', type='string', help='Will recursively scan a directory for files that contain SWFs. Must provide path in quotes')
  331 + parser.add_option('-c', '--compress', action='store_true', dest='compress', help='Compresses the SWF using Zlib')
  332 +
  333 + (options, args) = parser.parse_args()
  334 +
  335 + # Print help if no argurments are passed
  336 + if len(sys.argv) < 2:
  337 + parser.print_help()
  338 + return
  339 +
  340 + # Note files can't start with '-'
  341 + if '-' in sys.argv[len(sys.argv)-1][0] and options.PATH == None:
  342 + parser.print_help()
  343 + return
  344 +
  345 + # Recusive Search
  346 + if options.PATH != None:
  347 + paths = walk4SWF(options.PATH)
  348 + for y in paths:
  349 + #if sys.argv[0] not in y[0]:
  350 + try:
  351 + t = open(y[0], 'rb+')
  352 + disneyland(t, y[0],options)
  353 + except IOError:
  354 + pass
  355 + return
  356 +
  357 + # try to open file
  358 + try:
  359 + f = open(sys.argv[len(sys.argv)-1],'rb+')
  360 + filename = sys.argv[len(sys.argv)-1]
  361 + except Exception:
  362 + print '[ERROR] File can not be opended/accessed'
  363 + return
  364 +
  365 + disneyland(f,filename,options)
  366 + f.close()
  367 + return
  368 +
  369 +if __name__ == '__main__':
  370 + main()
  371 +
... ...
oletools/xxxswf2.py 0 → 100644
  1 +++ a/oletools/xxxswf2.py
  1 +#!/usr/bin/env python
  2 +"""
  3 +xxxswf2.py - Philippe Lagadec 2012-09-17
  4 +
  5 +xxxswf2 is a script to detect, extract and analyze Flash objects (SWF) that may
  6 +be embedded in files such as MS Office documents (e.g. Word, Excel),
  7 +which is especially useful for malware analysis.
  8 +xxxswf2 is an improved version of xxxswf.py published by Alexander Hanel on
  9 +http://hooked-on-mnemonics.blogspot.nl/2011/12/xxxswfpy.html
  10 +Compared to xxxswf, it can extract streams from MS Office documents by parsing
  11 +their OLE structure properly, which is necessary when streams are fragmented.
  12 +Stream fragmentation is a known obfuscation technique, as explained on
  13 +http://www.breakingpointsystems.com/resources/blog/evasion-with-ole2-fragmentation/
  14 +
  15 +xxxswf2 project website: http://www.decalage.info/python/xxxswf2
  16 +
  17 +xxxswf2 is copyright (c) 2012, Philippe Lagadec (http://www.decalage.info)
  18 +All rights reserved.
  19 +
  20 +Redistribution and use in source and binary forms, with or without modification,
  21 +are permitted provided that the following conditions are met:
  22 +
  23 + * Redistributions of source code must retain the above copyright notice, this
  24 + list of conditions and the following disclaimer.
  25 + * Redistributions in binary form must reproduce the above copyright notice,
  26 + this list of conditions and the following disclaimer in the documentation
  27 + and/or other materials provided with the distribution.
  28 +
  29 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  30 +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  31 +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  32 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  33 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  34 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  35 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  36 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  37 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  38 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  39 +"""
  40 +
  41 +__version__ = '0.01'
  42 +
  43 +#------------------------------------------------------------------------------
  44 +# CHANGELOG:
  45 +# 2012-09-17 v0.01 PL: - first version
  46 +
  47 +#------------------------------------------------------------------------------
  48 +# TODO:
  49 +# - check if file is OLE
  50 +# - support -r
  51 +
  52 +import optparse, sys, os
  53 +from thirdparty.xxxswf import xxxswf
  54 +from thirdparty.OleFileIO_PL import OleFileIO_PL
  55 +
  56 +def main():
  57 + # Scenarios:
  58 + # Scan file for SWF(s)
  59 + # Scan file for SWF(s) and extract them
  60 + # Scan file for SWF(s) and scan them with Yara
  61 + # Scan file for SWF(s), extract them and scan with Yara
  62 + # Scan directory recursively for files that contain SWF(s)
  63 + # Scan directory recursively for files that contain SWF(s) and extract them
  64 +
  65 + usage = 'usage: %prog [options] <file.bad>'
  66 + parser = optparse.OptionParser(usage=usage)
  67 + parser.add_option('-x', '--extract', action='store_true', dest='extract', help='Extracts the embedded SWF(s), names it MD5HASH.swf & saves it in the working dir. No addition args needed')
  68 + parser.add_option('-y', '--yara', action='store_true', dest='yara', help='Scans the SWF(s) with yara. If the SWF(s) is compressed it will be deflated. No addition args needed')
  69 + parser.add_option('-s', '--md5scan', action='store_true', dest='md5scan', help='Scans the SWF(s) for MD5 signatures. Please see func checkMD5 to define hashes. No addition args needed')
  70 + parser.add_option('-H', '--header', action='store_true', dest='header', help='Displays the SWFs file header. No addition args needed')
  71 + parser.add_option('-d', '--decompress', action='store_true', dest='decompress', help='Deflates compressed SWFS(s)')
  72 + parser.add_option('-r', '--recdir', dest='PATH', type='string', help='Will recursively scan a directory for files that contain SWFs. Must provide path in quotes')
  73 + parser.add_option('-c', '--compress', action='store_true', dest='compress', help='Compresses the SWF using Zlib')
  74 +
  75 + parser.add_option('-o', '--ole', action='store_true', dest='ole', help='Parse an OLE file (e.g. Word, Excel) to look for SWF in each stream')
  76 +
  77 +
  78 + (options, args) = parser.parse_args()
  79 +
  80 + # Print help if no argurments are passed
  81 + if len(args) == 0:
  82 + parser.print_help()
  83 + return
  84 +
  85 + if options.ole:
  86 + for filename in args:
  87 + ole = OleFileIO_PL.OleFileIO(filename)
  88 + for direntry in ole.direntries:
  89 + if direntry is not None and direntry.entry_type == OleFileIO_PL.STGTY_STREAM:
  90 + f = ole._open(direntry.isectStart, direntry.size)
  91 + # check if data contains the SWF magic: FWS or CWS
  92 + data = f.getvalue()
  93 + if 'FWS' in data or 'CWS' in data:
  94 + print 'OLE stream: %s' % repr(direntry.name)
  95 + # call xxxswf to scan or extract Flash files:
  96 + xxxswf.disneyland(f, direntry.name, options)
  97 + f.close()
  98 + ole.close()
  99 + else:
  100 + xxxswf.main()
  101 +
  102 +if __name__ == '__main__':
  103 + main()
... ...