Roll src/third_party/WebKit eac3800:0237a66 (svn 202606:202607)
[chromium-blink-merge.git] / chrome / tools / build / win / resedit.py
blob4bd53f53527523efdb29191fa993e969576b4d89
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.
6 """A utility script that can extract and edit resources in a Windows binary.
8 For detailed help, see the script's usage by invoking it with --help."""
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
22 _LOGGER = logging.getLogger(__name__)
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
37 UpdateResource.restype = ctypes.c_short
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
45 return res_id
48 class _ResourceEditor(object):
49 """A utility class to make it easy to extract and manipulate resources in a
50 Windows binary."""
52 def __init__(self, input_file, output_file):
53 """Create a new editor.
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
67 def __del__(self):
68 if self._module:
69 win32api.FreeLibrary(self._module)
70 self._module = None
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
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
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
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)
97 return self._temp_dir
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)
109 return self._update_handle
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)
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.
121 The generated directory hierarchy is three-level, and looks like:
122 resource-type/
123 resource-name/
124 lang-id.
126 Args:
127 extract_to: path to the folder to output to. This folder will be erased
128 and recreated if it already exists.
130 _LOGGER.info('Extracting all resources from "%s" to directory "%s".',
131 self.input_file, extract_to)
133 if os.path.exists(extract_to):
134 _LOGGER.info('Destination directory "%s" exists, deleting', extract_to)
135 shutil.rmtree(extract_to)
137 # Make sure the destination dir exists.
138 os.makedirs(extract_to)
140 # Now enumerate the resource types.
141 for res_type in win32api.EnumResourceTypes(self.module):
142 res_type_str = _ResIdToString(res_type)
144 # And the resource names.
145 for res_name in win32api.EnumResourceNames(self.module, res_type):
146 res_name_str = _ResIdToString(res_name)
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)
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)
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)
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.
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.
173 _LOGGER.info('Extracting resource "%s", lang "%d" name "%s" '
174 'to file "%s".', res_type, res_lang, res_name, dest_file)
176 data = win32api.LoadResource(self.module, res_type, res_name, res_lang)
177 with open(dest_file, 'wb') as f:
178 f.write(data)
180 def RemoveResource(self, res_type, res_lang, res_name):
181 """Removes a given resource, specified by type, language id and name.
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".
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,
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
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.
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.
212 _LOGGER.info('Writing resource "%s:%s" from file.',
213 res_type, res_name, file_path)
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);
222 self._modified = True
224 def Commit(self):
225 """Commit any successful resource edits this editor has performed.
227 This has the effect of writing the output file.
229 if self._update_handle:
230 update_handle = self._update_handle
231 self._update_handle = None
232 win32api.EndUpdateResource(update_handle, False)
234 _LOGGER.info('Writing edited file to "%s".', self._output_file)
235 shutil.copyfile(self._temp_file, self._output_file)
238 _USAGE = """\
239 usage: %prog [options] input_file
241 A utility script to extract and edit the resources in a Windows executable.
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
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
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.')
283 options, args = parser.parse_args()
285 if len(args) != 1:
286 parser.error('You have to specify an input file to work on.')
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.')
292 return options, args
295 def main(options, args):
296 """Main program for the script."""
297 if options.verbose:
298 logging.basicConfig(level=logging.INFO)
300 # Create the editor for our input file.
301 editor = _ResourceEditor(args[0], options.output_file)
303 if options.extract_all:
304 editor.ExtractAllToDir(options.extract_all)
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)
309 for res_type, res_lang, res_name in options.remove:
310 editor.RemoveResource(res_type, int(res_lang), res_name)
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)
315 if editor.modified:
316 editor.Commit()
319 if __name__ == '__main__':
320 sys.exit(main(*_ParseArgs()))