blob: 723c4b03422f283f9da20915effc15af1d99dbea [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2015 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.
if not os.path.exists(dest_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.
This is a legacy version of UpdateResourceData, where the data arg is read
from a file , rather than passed directly.
"""
_LOGGER.info('Writing resource from file %s', file_path)
with open(file_path, 'rb') as f:
self.UpdateResourceData(res_type, res_lang, res_name, f.read())
def UpdateResourceData(self, res_type, res_lang, res_name, data):
"""Inserts or updates a given resource with the given data.
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".
data: the new resource data.
"""
_LOGGER.info('Writing resource "%s:%s"', res_type, res_name)
win32api.UpdateResource(self.update_handle,
res_type,
res_name,
data,
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)
else:
_LOGGER.info('No edits made. Copying input to "%s".', self._output_file)
shutil.copyfile(self._input_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 _ConvertInts(*args):
"""Return args with any all-digit strings converted to ints."""
results = []
for arg in args:
if isinstance(arg, basestring) and arg.isdigit():
results.append(int(arg))
else:
results.append(arg)
return results
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:
res_type, res_lang, res_name = _ConvertInts(res_type, res_lang, res_name)
editor.ExtractResource(res_type, res_lang, res_name, dest_file)
for res_type, res_lang, res_name in options.remove:
res_type, res_lang, res_name = _ConvertInts(res_type, res_lang, res_name)
editor.RemoveResource(res_type, res_lang, res_name)
for res_type, res_lang, res_name, src_file in options.update:
res_type, res_lang, res_name = _ConvertInts(res_type, res_lang, res_name)
editor.UpdateResource(res_type, res_lang, res_name, src_file)
if editor.modified:
editor.Commit()
if __name__ == '__main__':
sys.exit(main(*_ParseArgs()))