diff options
author | siggi@chromium.org <siggi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-02 15:52:19 +0000 |
---|---|---|
committer | siggi@chromium.org <siggi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-02 15:52:19 +0000 |
commit | fb9069d24ec3f0a234d3a256ac5fc8d5d49bbdcc (patch) | |
tree | 8338425c02713c14114d0f53a916b4eb48eefa6c /chrome/tools | |
parent | be09131b34b3f71a871370652584e07b18a7a6fa (diff) | |
download | chromium_src-fb9069d24ec3f0a234d3a256ac5fc8d5d49bbdcc.zip chromium_src-fb9069d24ec3f0a234d3a256ac5fc8d5d49bbdcc.tar.gz chromium_src-fb9069d24ec3f0a234d3a256ac5fc8d5d49bbdcc.tar.bz2 |
A utility script to manipulate and extract resources from Windows programs.
This might be considered a first step in making our release signing process a little less opaque and brittle.
R=bradnelson@chromium.org,robertshield@chromium.org,grt@chromium.org
BUG=36234
Review URL: http://codereview.chromium.org/8775026
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@112707 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/tools')
-rw-r--r-- | chrome/tools/build/win/resedit.py | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/chrome/tools/build/win/resedit.py b/chrome/tools/build/win/resedit.py new file mode 100644 index 0000000..22724a3 --- /dev/null +++ b/chrome/tools/build/win/resedit.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +# Copyright (c) 2011 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())) |