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."""
11 import ctypes
.wintypes
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):
48 class _ResourceEditor(object):
49 """A utility class to make it easy to extract and manipulate resources in a
52 def __init__(self
, input_file
, output_file
):
53 """Create a new editor.
56 input_file: path to the input file.
57 output_file: (optional) path to the output file.
59 self
._input
_file
= input_file
60 self
._output
_file
= output_file
61 self
._modified
= False
64 self
._temp
_file
= None
65 self
._update
_handle
= None
69 win32api
.FreeLibrary(self
._module
)
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
78 _LOGGER
.info('Removing temporary directory "%s".', self
._temp
_dir
)
79 shutil
.rmtree(self
._temp
_dir
)
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
)
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
)
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:
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
,
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" '
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,
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
:
180 def RemoveResource(self
, res_type
, res_lang
, res_name
):
181 """Removes a given resource, specified by type, language id and name.
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
,
196 # Raise an error on failure.
198 error
= win32api
.GetLastError()
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.
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
,
222 self
._modified
= True
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
)
239 usage: %prog [options] input_file
241 A utility script to extract and edit the resources in a Windows executable.
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
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 '
283 options
, args
= parser
.parse_args()
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.')
295 def main(options
, args
):
296 """Main program for the script."""
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
)
319 if __name__
== '__main__':
320 sys
.exit(main(*_ParseArgs()))