#!/usr/bin/env python # Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """A utility script that can extract and edit resources in a Windows binary. For detailed help, see the script's usage by invoking it with --help.""" import ctypes import ctypes.wintypes import logging import optparse import os import shutil import sys import tempfile import win32api import win32con _LOGGER = logging.getLogger(__name__) # The win32api-supplied UpdateResource wrapper unfortunately does not allow # one to remove resources due to overzealous parameter verification. # For that case we're forced to go straight to the native API implementation. UpdateResource = ctypes.windll.kernel32.UpdateResourceW UpdateResource.argtypes = [ ctypes.wintypes.HANDLE, # HANDLE hUpdate ctypes.c_wchar_p, # LPCTSTR lpType ctypes.c_wchar_p, # LPCTSTR lpName ctypes.c_short, # WORD wLanguage ctypes.c_void_p, # LPVOID lpData ctypes.c_ulong, # DWORD cbData ] UpdateResource.restype = ctypes.c_short def _ResIdToString(res_id): # Convert integral res types/ids to a string. if isinstance(res_id, int): return "#%d" % res_id return res_id class _ResourceEditor(object): """A utility class to make it easy to extract and manipulate resources in a Windows binary.""" def __init__(self, input_file, output_file): """Create a new editor. Args: input_file: path to the input file. output_file: (optional) path to the output file. """ self._input_file = input_file self._output_file = output_file self._modified = False self._module = None self._temp_dir = None self._temp_file = None self._update_handle = None def __del__(self): if self._module: win32api.FreeLibrary(self._module) self._module = None if self._update_handle: _LOGGER.info('Canceling edits to "%s".', self.input_file) win32api.EndUpdateResource(self._update_handle, False) self._update_handle = None if self._temp_dir: _LOGGER.info('Removing temporary directory "%s".', self._temp_dir) shutil.rmtree(self._temp_dir) self._temp_dir = None def _GetModule(self): if not self._module: # Specify a full path to LoadLibraryEx to prevent # it from searching the path. input_file = os.path.abspath(self.input_file) _LOGGER.info('Loading input_file from "%s"', input_file) self._module = win32api.LoadLibraryEx( input_file, None, win32con.LOAD_LIBRARY_AS_DATAFILE) return self._module def _GetTempDir(self): if not self._temp_dir: self._temp_dir = tempfile.mkdtemp() _LOGGER.info('Created temporary directory "%s".', self._temp_dir) return self._temp_dir def _GetUpdateHandle(self): if not self._update_handle: # Make a copy of the input file in the temp dir. self._temp_file = os.path.join(self.temp_dir, os.path.basename(self._input_file)) shutil.copyfile(self._input_file, self._temp_file) # Open a resource update handle on the copy. _LOGGER.info('Opening temp file "%s".', self._temp_file) self._update_handle = win32api.BeginUpdateResource(self._temp_file, False) return self._update_handle modified = property(lambda self: self._modified) input_file = property(lambda self: self._input_file) module = property(_GetModule) temp_dir = property(_GetTempDir) update_handle = property(_GetUpdateHandle) def ExtractAllToDir(self, extract_to): """Extracts all resources from our input file to a directory hierarchy in the directory named extract_to. The generated directory hierarchy is three-level, and looks like: resource-type/ resource-name/ lang-id. Args: extract_to: path to the folder to output to. This folder will be erased and recreated if it already exists. """ _LOGGER.info('Extracting all resources from "%s" to directory "%s".', self.input_file, extract_to) if os.path.exists(extract_to): _LOGGER.info('Destination directory "%s" exists, deleting', extract_to) shutil.rmtree(extract_to) # Make sure the destination dir exists. os.makedirs(extract_to) # Now enumerate the resource types. for res_type in win32api.EnumResourceTypes(self.module): res_type_str = _ResIdToString(res_type) # And the resource names. for res_name in win32api.EnumResourceNames(self.module, res_type): res_name_str = _ResIdToString(res_name) # Then the languages. for res_lang in win32api.EnumResourceLanguages(self.module, res_type, res_name): res_lang_str = _ResIdToString(res_lang) dest_dir = os.path.join(extract_to, res_type_str, res_lang_str) dest_file = os.path.join(dest_dir, res_name_str) _LOGGER.info('Extracting resource "%s", lang "%d" name "%s" ' 'to file "%s".', res_type_str, res_lang, res_name_str, dest_file) # Extract each resource to a file in the output dir. os.makedirs(dest_dir) self.ExtractResource(res_type, res_lang, res_name, dest_file) def ExtractResource(self, res_type, res_lang, res_name, dest_file): """Extracts a given resource, specified by type, language id and name, to a given file. Args: res_type: the type of the resource, e.g. "B7". res_lang: the language id of the resource e.g. 1033. res_name: the name of the resource, e.g. "SETUP.EXE". dest_file: path to the file where the resource data will be written. """ _LOGGER.info('Extracting resource "%s", lang "%d" name "%s" ' 'to file "%s".', res_type, res_lang, res_name, dest_file) data = win32api.LoadResource(self.module, res_type, res_name, res_lang) with open(dest_file, 'wb') as f: f.write(data) def RemoveResource(self, res_type, res_lang, res_name): """Removes a given resource, specified by type, language id and name. Args: res_type: the type of the resource, e.g. "B7". res_lang: the language id of the resource, e.g. 1033. res_name: the name of the resource, e.g. "SETUP.EXE". """ _LOGGER.info('Removing resource "%s:%s".', res_type, res_name) # We have to go native to perform a removal. ret = UpdateResource(self.update_handle, res_type, res_name, res_lang, None, 0) # Raise an error on failure. if ret == 0: error = win32api.GetLastError() print "error", error raise RuntimeError(error) self._modified = True def UpdateResource(self, res_type, res_lang, res_name, file_path): """Inserts or updates a given resource with the contents of a file. Args: res_type: the type of the resource, e.g. "B7". res_lang: the language id of the resource, e.g. 1033. res_name: the name of the resource, e.g. "SETUP.EXE". file_path: path to the file containing the new resource data. """ _LOGGER.info('Writing resource "%s:%s" from file.', res_type, res_name, file_path) with open(file_path, 'rb') as f: win32api.UpdateResource(self.update_handle, res_type, res_name, f.read(), res_lang); self._modified = True def Commit(self): """Commit any successful resource edits this editor has performed. This has the effect of writing the output file. """ if self._update_handle: update_handle = self._update_handle self._update_handle = None win32api.EndUpdateResource(update_handle, False) _LOGGER.info('Writing edited file to "%s".', self._output_file) shutil.copyfile(self._temp_file, self._output_file) _USAGE = """\ usage: %prog [options] input_file A utility script to extract and edit the resources in a Windows executable. EXAMPLE USAGE: # Extract from mini_installer.exe, the resource type "B7", langid 1033 and # name "CHROME.PACKED.7Z" to a file named chrome.7z. # Note that 1033 corresponds to English (United States). %prog mini_installer.exe --extract B7 1033 CHROME.PACKED.7Z chrome.7z # Update mini_installer.exe by removing the resouce type "BL", langid 1033 and # name "SETUP.EXE". Add the resource type "B7", langid 1033 and name # "SETUP.EXE.packed.7z" from the file setup.packed.7z. # Write the edited file to mini_installer_packed.exe. %prog mini_installer.exe \\ --remove BL 1033 SETUP.EXE \\ --update B7 1033 SETUP.EXE.packed.7z setup.packed.7z \\ --output-file mini_installer_packed.exe """ def _ParseArgs(): parser = optparse.OptionParser(_USAGE) parser.add_option('', '--verbose', action='store_true', help='Enable verbose logging.') parser.add_option('', '--extract_all', help='Path to a folder which will be created, in which all resources ' 'from the input_file will be stored, each in a file named ' '"res_type/lang_id/res_name".') parser.add_option('', '--extract', action='append', default=[], nargs=4, help='Extract the resource with the given type, language id and name ' 'to the given file.', metavar='type langid name file_path') parser.add_option('', '--remove', action='append', default=[], nargs=3, help='Remove the resource with the given type, langid and name.', metavar='type langid name') parser.add_option('', '--update', action='append', default=[], nargs=4, help='Insert or update the resource with the given type, langid and ' 'name with the contents of the file given.', metavar='type langid name file_path') parser.add_option('', '--output_file', help='On success, OUTPUT_FILE will be written with a copy of the ' 'input file with the edits specified by any remove or update ' 'options.') options, args = parser.parse_args() if len(args) != 1: parser.error('You have to specify an input file to work on.') modify = options.remove or options.update if modify and not options.output_file: parser.error('You have to specify an output file with edit options.') return options, args def main(options, args): """Main program for the script.""" if options.verbose: logging.basicConfig(level=logging.INFO) # Create the editor for our input file. editor = _ResourceEditor(args[0], options.output_file) if options.extract_all: editor.ExtractAllToDir(options.extract_all) for res_type, res_lang, res_name, dest_file in options.extract: editor.ExtractResource(res_type, int(res_lang), res_name, dest_file) for res_type, res_lang, res_name in options.remove: editor.RemoveResource(res_type, int(res_lang), res_name) for res_type, res_lang, res_name, src_file in options.update: editor.UpdateResource(res_type, int(res_lang), res_name, src_file) if editor.modified: editor.Commit() if __name__ == '__main__': sys.exit(main(*_ParseArgs()))