Home | History | Annotate | Download | only in win
      1 #!/usr/bin/env python
      2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """A utility script that can extract and edit resources in a Windows binary.
      7 
      8 For detailed help, see the script's usage by invoking it with --help."""
      9 
     10 import ctypes
     11 import ctypes.wintypes
     12 import logging
     13 import optparse
     14 import os
     15 import shutil
     16 import sys
     17 import tempfile
     18 import win32api
     19 import win32con
     20 
     21 
     22 _LOGGER = logging.getLogger(__name__)
     23 
     24 
     25 # The win32api-supplied UpdateResource wrapper unfortunately does not allow
     26 # one to remove resources due to overzealous parameter verification.
     27 # For that case we're forced to go straight to the native API implementation.
     28 UpdateResource = ctypes.windll.kernel32.UpdateResourceW
     29 UpdateResource.argtypes = [
     30     ctypes.wintypes.HANDLE,  # HANDLE hUpdate
     31     ctypes.c_wchar_p,  # LPCTSTR lpType
     32     ctypes.c_wchar_p,  # LPCTSTR lpName
     33     ctypes.c_short,  # WORD wLanguage
     34     ctypes.c_void_p,  # LPVOID lpData
     35     ctypes.c_ulong,  # DWORD cbData
     36     ]
     37 UpdateResource.restype = ctypes.c_short
     38 
     39 
     40 def _ResIdToString(res_id):
     41   # Convert integral res types/ids to a string.
     42   if isinstance(res_id, int):
     43     return "#%d" % res_id
     44 
     45   return res_id
     46 
     47 
     48 class _ResourceEditor(object):
     49   """A utility class to make it easy to extract and manipulate resources in a
     50   Windows binary."""
     51 
     52   def __init__(self, input_file, output_file):
     53     """Create a new editor.
     54 
     55     Args:
     56         input_file: path to the input file.
     57         output_file: (optional) path to the output file.
     58     """
     59     self._input_file = input_file
     60     self._output_file = output_file
     61     self._modified = False
     62     self._module = None
     63     self._temp_dir = None
     64     self._temp_file = None
     65     self._update_handle = None
     66 
     67   def __del__(self):
     68     if self._module:
     69       win32api.FreeLibrary(self._module)
     70       self._module = None
     71 
     72     if self._update_handle:
     73       _LOGGER.info('Canceling edits to "%s".', self.input_file)
     74       win32api.EndUpdateResource(self._update_handle, False)
     75       self._update_handle = None
     76 
     77     if self._temp_dir:
     78       _LOGGER.info('Removing temporary directory "%s".', self._temp_dir)
     79       shutil.rmtree(self._temp_dir)
     80       self._temp_dir = None
     81 
     82   def _GetModule(self):
     83     if not self._module:
     84       # Specify a full path to LoadLibraryEx to prevent
     85       # it from searching the path.
     86       input_file = os.path.abspath(self.input_file)
     87       _LOGGER.info('Loading input_file from "%s"', input_file)
     88       self._module = win32api.LoadLibraryEx(
     89           input_file, None, win32con.LOAD_LIBRARY_AS_DATAFILE)
     90     return self._module
     91 
     92   def _GetTempDir(self):
     93     if not self._temp_dir:
     94       self._temp_dir = tempfile.mkdtemp()
     95       _LOGGER.info('Created temporary directory "%s".', self._temp_dir)
     96 
     97     return self._temp_dir
     98 
     99   def _GetUpdateHandle(self):
    100     if not self._update_handle:
    101       # Make a copy of the input file in the temp dir.
    102       self._temp_file = os.path.join(self.temp_dir,
    103                                      os.path.basename(self._input_file))
    104       shutil.copyfile(self._input_file, self._temp_file)
    105       # Open a resource update handle on the copy.
    106       _LOGGER.info('Opening temp file "%s".', self._temp_file)
    107       self._update_handle = win32api.BeginUpdateResource(self._temp_file, False)
    108 
    109     return self._update_handle
    110 
    111   modified = property(lambda self: self._modified)
    112   input_file = property(lambda self: self._input_file)
    113   module = property(_GetModule)
    114   temp_dir = property(_GetTempDir)
    115   update_handle = property(_GetUpdateHandle)
    116 
    117   def ExtractAllToDir(self, extract_to):
    118     """Extracts all resources from our input file to a directory hierarchy
    119     in the directory named extract_to.
    120 
    121     The generated directory hierarchy is three-level, and looks like:
    122       resource-type/
    123         resource-name/
    124           lang-id.
    125 
    126     Args:
    127       extract_to: path to the folder to output to. This folder will be erased
    128           and recreated if it already exists.
    129     """
    130     _LOGGER.info('Extracting all resources from "%s" to directory "%s".',
    131         self.input_file, extract_to)
    132 
    133     if os.path.exists(extract_to):
    134       _LOGGER.info('Destination directory "%s" exists, deleting', extract_to)
    135       shutil.rmtree(extract_to)
    136 
    137     # Make sure the destination dir exists.
    138     os.makedirs(extract_to)
    139 
    140     # Now enumerate the resource types.
    141     for res_type in win32api.EnumResourceTypes(self.module):
    142       res_type_str = _ResIdToString(res_type)
    143 
    144       # And the resource names.
    145       for res_name in win32api.EnumResourceNames(self.module, res_type):
    146         res_name_str = _ResIdToString(res_name)
    147 
    148         # Then the languages.
    149         for res_lang in win32api.EnumResourceLanguages(self.module,
    150             res_type, res_name):
    151           res_lang_str = _ResIdToString(res_lang)
    152 
    153           dest_dir = os.path.join(extract_to, res_type_str, res_lang_str)
    154           dest_file = os.path.join(dest_dir, res_name_str)
    155           _LOGGER.info('Extracting resource "%s", lang "%d" name "%s" '
    156                        'to file "%s".',
    157                        res_type_str, res_lang, res_name_str, dest_file)
    158 
    159           # Extract each resource to a file in the output dir.
    160           os.makedirs(dest_dir)
    161           self.ExtractResource(res_type, res_lang, res_name, dest_file)
    162 
    163   def ExtractResource(self, res_type, res_lang, res_name, dest_file):
    164     """Extracts a given resource, specified by type, language id and name,
    165     to a given file.
    166 
    167     Args:
    168       res_type: the type of the resource, e.g. "B7".
    169       res_lang: the language id of the resource e.g. 1033.
    170       res_name: the name of the resource, e.g. "SETUP.EXE".
    171       dest_file: path to the file where the resource data will be written.
    172     """
    173     _LOGGER.info('Extracting resource "%s", lang "%d" name "%s" '
    174                  'to file "%s".', res_type, res_lang, res_name, dest_file)
    175 
    176     data = win32api.LoadResource(self.module, res_type, res_name, res_lang)
    177     with open(dest_file, 'wb') as f:
    178       f.write(data)
    179 
    180   def RemoveResource(self, res_type, res_lang, res_name):
    181     """Removes a given resource, specified by type, language id and name.
    182 
    183     Args:
    184       res_type: the type of the resource, e.g. "B7".
    185       res_lang: the language id of the resource, e.g. 1033.
    186       res_name: the name of the resource, e.g. "SETUP.EXE".
    187     """
    188     _LOGGER.info('Removing resource "%s:%s".', res_type, res_name)
    189     # We have to go native to perform a removal.
    190     ret = UpdateResource(self.update_handle,
    191                          res_type,
    192                          res_name,
    193                          res_lang,
    194                          None,
    195                          0)
    196     # Raise an error on failure.
    197     if ret == 0:
    198       error = win32api.GetLastError()
    199       print "error", error
    200       raise RuntimeError(error)
    201     self._modified = True
    202 
    203   def UpdateResource(self, res_type, res_lang, res_name, file_path):
    204     """Inserts or updates a given resource with the contents of a file.
    205 
    206     Args:
    207       res_type: the type of the resource, e.g. "B7".
    208       res_lang: the language id of the resource, e.g. 1033.
    209       res_name: the name of the resource, e.g. "SETUP.EXE".
    210       file_path: path to the file containing the new resource data.
    211     """
    212     _LOGGER.info('Writing resource "%s:%s" from file.',
    213         res_type, res_name, file_path)
    214 
    215     with open(file_path, 'rb') as f:
    216       win32api.UpdateResource(self.update_handle,
    217                               res_type,
    218                               res_name,
    219                               f.read(),
    220                               res_lang);
    221 
    222     self._modified = True
    223 
    224   def Commit(self):
    225     """Commit any successful resource edits this editor has performed.
    226 
    227     This has the effect of writing the output file.
    228     """
    229     if self._update_handle:
    230       update_handle = self._update_handle
    231       self._update_handle = None
    232       win32api.EndUpdateResource(update_handle, False)
    233 
    234     _LOGGER.info('Writing edited file to "%s".', self._output_file)
    235     shutil.copyfile(self._temp_file, self._output_file)
    236 
    237 
    238 _USAGE = """\
    239 usage: %prog [options] input_file
    240 
    241 A utility script to extract and edit the resources in a Windows executable.
    242 
    243 EXAMPLE USAGE:
    244 # Extract from mini_installer.exe, the resource type "B7", langid 1033 and
    245 # name "CHROME.PACKED.7Z" to a file named chrome.7z.
    246 # Note that 1033 corresponds to English (United States).
    247 %prog mini_installer.exe --extract B7 1033 CHROME.PACKED.7Z chrome.7z
    248 
    249 # Update mini_installer.exe by removing the resouce type "BL", langid 1033 and
    250 # name "SETUP.EXE". Add the resource type "B7", langid 1033 and name
    251 # "SETUP.EXE.packed.7z" from the file setup.packed.7z.
    252 # Write the edited file to mini_installer_packed.exe.
    253 %prog mini_installer.exe \\
    254     --remove BL 1033 SETUP.EXE \\
    255     --update B7 1033 SETUP.EXE.packed.7z setup.packed.7z \\
    256     --output-file mini_installer_packed.exe
    257 """
    258 
    259 def _ParseArgs():
    260   parser = optparse.OptionParser(_USAGE)
    261   parser.add_option('', '--verbose', action='store_true',
    262       help='Enable verbose logging.')
    263   parser.add_option('', '--extract_all',
    264       help='Path to a folder which will be created, in which all resources '
    265            'from the input_file will be stored, each in a file named '
    266            '"res_type/lang_id/res_name".')
    267   parser.add_option('', '--extract', action='append', default=[], nargs=4,
    268       help='Extract the resource with the given type, language id and name '
    269            'to the given file.',
    270       metavar='type langid name file_path')
    271   parser.add_option('', '--remove', action='append', default=[], nargs=3,
    272       help='Remove the resource with the given type, langid and name.',
    273       metavar='type langid name')
    274   parser.add_option('', '--update', action='append', default=[], nargs=4,
    275       help='Insert or update the resource with the given type, langid and '
    276            'name with the contents of the file given.',
    277       metavar='type langid name file_path')
    278   parser.add_option('', '--output_file',
    279     help='On success, OUTPUT_FILE will be written with a copy of the '
    280          'input file with the edits specified by any remove or update '
    281          'options.')
    282 
    283   options, args = parser.parse_args()
    284 
    285   if len(args) != 1:
    286     parser.error('You have to specify an input file to work on.')
    287 
    288   modify = options.remove or options.update
    289   if modify and not options.output_file:
    290     parser.error('You have to specify an output file with edit options.')
    291 
    292   return options, args
    293 
    294 
    295 def main(options, args):
    296   """Main program for the script."""
    297   if options.verbose:
    298     logging.basicConfig(level=logging.INFO)
    299 
    300   # Create the editor for our input file.
    301   editor = _ResourceEditor(args[0], options.output_file)
    302 
    303   if options.extract_all:
    304     editor.ExtractAllToDir(options.extract_all)
    305 
    306   for res_type, res_lang, res_name, dest_file in options.extract:
    307     editor.ExtractResource(res_type, int(res_lang), res_name, dest_file)
    308 
    309   for res_type, res_lang, res_name in options.remove:
    310     editor.RemoveResource(res_type, int(res_lang), res_name)
    311 
    312   for res_type, res_lang, res_name, src_file in options.update:
    313     editor.UpdateResource(res_type, int(res_lang), res_name, src_file)
    314 
    315   if editor.modified:
    316     editor.Commit()
    317 
    318 
    319 if __name__ == '__main__':
    320   sys.exit(main(*_ParseArgs()))
    321