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. 5 6 """A utility script that can extract and edit resources in a Windows binary. 7 8 For detailed help, see the script's usage by invoking it with --help.""" 9 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 20 21 22 _LOGGER = logging.getLogger(__name__) 23 24 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 36 ] 37 UpdateResource.restype = ctypes.c_short 38 39 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 44 45 return res_id 46 47 48 class _ResourceEditor(object): 49 """A utility class to make it easy to extract and manipulate resources in a 50 Windows binary.""" 51 52 def __init__(self, input_file, output_file): 53 """Create a new editor. 54 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 66 67 def __del__(self): 68 if self._module: 69 win32api.FreeLibrary(self._module) 70 self._module = None 71 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 76 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 81 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 91 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) 96 97 return self._temp_dir 98 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) 108 109 return self._update_handle 110 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) 116 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. 120 121 The generated directory hierarchy is three-level, and looks like: 122 resource-type/ 123 resource-name/ 124 lang-id. 125 126 Args: 127 extract_to: path to the folder to output to. This folder will be erased 128 and recreated if it already exists. 129 """ 130 _LOGGER.info('Extracting all resources from "%s" to directory "%s".', 131 self.input_file, extract_to) 132 133 if os.path.exists(extract_to): 134 _LOGGER.info('Destination directory "%s" exists, deleting', extract_to) 135 shutil.rmtree(extract_to) 136 137 # Make sure the destination dir exists. 138 os.makedirs(extract_to) 139 140 # Now enumerate the resource types. 141 for res_type in win32api.EnumResourceTypes(self.module): 142 res_type_str = _ResIdToString(res_type) 143 144 # And the resource names. 145 for res_name in win32api.EnumResourceNames(self.module, res_type): 146 res_name_str = _ResIdToString(res_name) 147 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) 152 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) 158 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) 162 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. 166 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. 172 """ 173 _LOGGER.info('Extracting resource "%s", lang "%d" name "%s" ' 174 'to file "%s".', res_type, res_lang, res_name, dest_file) 175 176 data = win32api.LoadResource(self.module, res_type, res_name, res_lang) 177 with open(dest_file, 'wb') as f: 178 f.write(data) 179 180 def RemoveResource(self, res_type, res_lang, res_name): 181 """Removes a given resource, specified by type, language id and name. 182 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". 187 """ 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, 195 0) 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 202 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. 205 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. 211 """ 212 _LOGGER.info('Writing resource "%s:%s" from file.', 213 res_type, res_name, file_path) 214 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); 221 222 self._modified = True 223 224 def Commit(self): 225 """Commit any successful resource edits this editor has performed. 226 227 This has the effect of writing the output file. 228 """ 229 if self._update_handle: 230 update_handle = self._update_handle 231 self._update_handle = None 232 win32api.EndUpdateResource(update_handle, False) 233 234 _LOGGER.info('Writing edited file to "%s".', self._output_file) 235 shutil.copyfile(self._temp_file, self._output_file) 236 237 238 _USAGE = """\ 239 usage: %prog [options] input_file 240 241 A utility script to extract and edit the resources in a Windows executable. 242 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 248 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 257 """ 258 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.') 282 283 options, args = parser.parse_args() 284 285 if len(args) != 1: 286 parser.error('You have to specify an input file to work on.') 287 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.') 291 292 return options, args 293 294 295 def main(options, args): 296 """Main program for the script.""" 297 if options.verbose: 298 logging.basicConfig(level=logging.INFO) 299 300 # Create the editor for our input file. 301 editor = _ResourceEditor(args[0], options.output_file) 302 303 if options.extract_all: 304 editor.ExtractAllToDir(options.extract_all) 305 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) 308 309 for res_type, res_lang, res_name in options.remove: 310 editor.RemoveResource(res_type, int(res_lang), res_name) 311 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) 314 315 if editor.modified: 316 editor.Commit() 317 318 319 if __name__ == '__main__': 320 sys.exit(main(*_ParseArgs())) 321