Chromium Code Reviews| Index: chrome/tools/build/win/resedit.py |
| diff --git a/chrome/tools/build/win/resedit.py b/chrome/tools/build/win/resedit.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..f96f002c6ff0bba98aad21ef546bc280f57e460a |
| --- /dev/null |
| +++ b/chrome/tools/build/win/resedit.py |
| @@ -0,0 +1,318 @@ |
| +#!/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 |
|
grt (UTC plus 2)
2011/12/02 02:38:11
is there an appropriate place to file a bug for th
Sigurður Ásgeirsson
2011/12/02 13:57:45
I don't know. This'd be in the win32api module imp
|
| +# one to remove resources due to overzeolus parameter verification. |
|
robertshield
2011/12/02 00:35:42
overzealous
Sigurður Ásgeirsson
2011/12/02 13:57:45
Done.
|
| +# For that case we're foced to go straight to the native API implementation. |
|
robertshield
2011/12/02 00:35:42
forced
Sigurður Ásgeirsson
2011/12/02 13:57:45
Done.
|
| +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._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) |
|
robertshield
2011/12/02 00:35:42
does this raise an exception with a readable error
Sigurður Ásgeirsson
2011/12/02 13:57:45
Yups, you get the API call, the error code and the
|
| + 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. |
| + file_copy = os.path.join(self.temp_dir, |
|
grt (UTC plus 2)
2011/12/02 02:38:11
how about keeping this value in the instance so yo
Sigurður Ásgeirsson
2011/12/02 13:57:45
Done.
|
| + os.path.basename(self._input_file)) |
| + shutil.copyfile(self._input_file, file_copy) |
| + # Open a resource update handle on the copy. |
| + _LOGGER.info('Opening temp file "%s".', file_copy) |
| + self._update_handle = win32api.BeginUpdateResource(file_copy, 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) |
| + with open(dest_file, 'wb') as f: |
| + f.write(win32api.LoadResource(self.module, res_type, res_name)) |
|
grt (UTC plus 2)
2011/12/02 02:38:11
This appears to be using FindResource rather than
Sigurður Ásgeirsson
2011/12/02 13:57:45
It does take a langid as an optional param, my ove
|
| + |
| + def ExtractResource(self, res_type, res_name, dest_file): |
| + """Extracts a given resource, specified by type and name, to a given file. |
| + |
| + Args: |
| + res_type: the type of the resource, e.g. "B7". |
| + 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:%s" to file "%s".', |
| + res_type, res_name, dest_file) |
| + |
| + with open(dest_file, 'wb') as f: |
| + f.write(win32api.LoadResource(self.module, res_type, res_name)) |
|
grt (UTC plus 2)
2011/12/02 02:38:11
See previous comment.
Sigurður Ásgeirsson
2011/12/02 13:57:45
Done.
|
| + |
| + 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) |
| + |
| + temp_file = os.path.join(self.temp_dir, os.path.basename(self.input_file)) |
| + _LOGGER.info('Writing edited file to "%s".', self._output_file) |
| + shutil.copyfile(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 |
|
grt (UTC plus 2)
2011/12/02 02:38:11
Either remove references to langid and 1033, or fi
Sigurður Ásgeirsson
2011/12/02 13:57:45
Done.
|
| +# 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=3, |
| + help='Extract the resource with the given type and name to the given ' |
| + 'file.', |
| + metavar='type name') |
| + 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 written with a copy of the ' |
|
robertshield
2011/12/02 00:35:42
will be written
Sigurður Ásgeirsson
2011/12/02 13:57:45
Done.
|
| + '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_name, dest_file in options.extract: |
| + editor.ExtractResource(res_type, 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())) |