├── .gitignore ├── README.txt ├── gui.py ├── guires.py ├── lzari.py ├── mymc.py ├── ps2mc.py ├── ps2mc_dir.py ├── ps2mc_ecc.py ├── ps2save.py ├── round.py ├── sjistab.py └── verbuild.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | README.txt 2 | 3 | By Ross Ridge 4 | Pubic Domain 5 | 6 | @(#) mymc README.txt 1.6 12/10/04 19:18:08 7 | 8 | 9 | This file describes mymc, a utility for manipulating PlayStation 2 10 | memory card images as used by the emulator PCSX2. Its main purpose is 11 | to allow save games to be imported and exported to and from these 12 | images. Both MAX Drive and EMS (.psu) save files are fully supported, 13 | however save files in the SharkPort/X-Port and Code Breaker formats 14 | can only be imported and not exported. In addition to these basic 15 | functions, mymc can also perform a number of other operations, like 16 | creating new memory card images, viewing their contents, and adding 17 | and extracting individual files. 18 | 19 | A simple, hopefully easy to use, graphicial user interface (GUI) is 20 | provided, but it's limitted to only basic operations. More advanced 21 | opterations require the use of a command line tool. To install mymc, 22 | unpack the downloaded ZIP archive to a new directory on your machine. 23 | You can then run the GUI version of mymc by openning that newn 24 | directory with Windows Explorer and double clicking on the "mymc-gui" 25 | icon. To make it easier to access, you can drag the "mymc-gui" icon 26 | to either your Desktop, Start Menu or Quick Launch toolbar. Make sure 27 | if you do so, that you create a shortcut to "mymc-gui.exe". If you 28 | copy the file instead, the program won't work. 29 | 30 | The command line utility can be invoked from the Windows Command 31 | Prompt by using the "mymc" command. The executable "mymc.exe" and 32 | number of support files and these file must kept together in the same 33 | directory. To run the command you need to either add the directory 34 | where you unpacked the distribution to your PATH or type the full 35 | pathname of the executable. For example if you unpacked mymc to a 36 | directory named "c:\mymc" you need to enter "c:\mymc\mymc.exe" to run 37 | the program. 38 | 39 | The second important thing to note is that mymc is only "alpha" 40 | quality software. This means that has is been released without 41 | extensive testing and may be unreliable. While it works fine for me, 42 | the author, it might not work as well for you. For that reason you 43 | should be careful how you use it, and prepared for the eventuality of 44 | it corrupting your save game images or producing garbage save files. 45 | If you're worried about this, one make things safer is to use two 46 | memory card images. Use the first image to load and save your games 47 | with under PCSX2, and the second image to import and export saves 48 | games using mysc. Then use the PS2 browser to copy files between two 49 | card images. 50 | 51 | 52 | GUI TUTORIAL 53 | ============ 54 | 55 | The GUI for mymc is should be easy to use. After starting mymc, you 56 | can select the PS2 memory card image you want to work with by 57 | selecting the "Open" command by pressing the first button on the 58 | toolbar. You can then import a save file clicking on the Import 59 | toolbar button. To export a save files, first select it and then 60 | press the Export button. You can delete a save file permanently from 61 | your memory card, by selecting the "Delete" command from the File 62 | menu. 63 | 64 | Do not try to use mymc to modify a memory card image while PCSX2 is 65 | running. Doing so will corrupt your memory card. 66 | 67 | 68 | COMMAND LINE TUTORIAL 69 | ===================== 70 | 71 | The basic usage template for mysc is "mymc memcard.ps2 command". The 72 | first argument, "memcard.ps2" is the filename of the memory card image 73 | while "command" is the name of the command you wish to use on the 74 | image. So for example, assuming you've installed mymc in "c:\mymc" 75 | and you've installed PCSX2 in "c:\pcsx2" you could enter the following 76 | command to see the contents of the memory card in the emulator's slot 77 | 1: 78 | 79 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 dir 80 | 81 | You would see output something like this: 82 | 83 | BASLUS-20678USAGAS00 UNLIMITED SAGA 84 | 154KB Not Protected SYSTEMDATA 85 | 86 | BADATA-SYSTEM Your System 87 | 5KB Not Protected Configuration 88 | 89 | BASLUS-20488-0000D SOTET<13>060:08 90 | 173KB Not Protected Arias 91 | 92 | 7,800 KB Free 93 | 94 | This is the simple "user friendly" way to view the contents of a 95 | memory card. It displays the same information you can see using the 96 | PlayStation 2 memory card browser. On the right is name of each save, 97 | and on the left is the size and protection status of the save. Also 98 | on the left is one bit of information you won't see in the browser, 99 | the directory name of the save file. PlayStation 2 saves are actually 100 | a collection of different files all stored in a single directory on 101 | the memory card. This is important information, because you need to 102 | know it to export save files. 103 | 104 | As mentioned above, if you know the directory name of a save, you can 105 | export it. Exporting a save creates a save file in either the EMS 106 | (.psu) or MAX Drive (.max) format. You can then transfer the save to 107 | real PS2 memory using the appropriate tools. You can also send the 108 | saves to someone else to use or just keep them on your hard drive as a 109 | backup. The following command demonstrates how to export a save in 110 | the EMS format using mymc: 111 | 112 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 export BASLUS-20448-0000D 113 | 114 | This will create a file called "BASLUS-20448-0000D.psu" in the current 115 | directory. To create a file in the MAX format instead, use the export 116 | command's -m option: 117 | 118 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 export -m BASLUS-20448-0000D 119 | 120 | This creates a file named "BASLUS-20448-0000D.max". Note the "-m" 121 | option that appears after the "export" command. 122 | 123 | Importing save files is similar. The save file type is auto-detected, 124 | so you don't need use an "-m" option with MAX Drive saves. Here's a 125 | couple of examples using each format: 126 | 127 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 import BASLUS-20035.psu 128 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 import 20062_3583_GTA3.max 129 | 130 | 131 | ADVANCED NOTES 132 | ============== 133 | 134 | - To get general help with the command line utility use the "-h" 135 | global option (eg. "mymc -h"). To get help with a specific 136 | command use the "-h" option with that command (eg. "mymc x 137 | import -h"). In this later case, you need to specify a memory 138 | card image file, but it's ignored and so doesn't need to exist. 139 | 140 | - Both executables in the Windows version, "mymc.exe" and 141 | "mymc-gui.exe" do the same thing and support the same options. 142 | The difference is that "mymc" is console application, while 143 | "mymc-gui" is a Windows appliction. Currently, using "mymc" 144 | to start the GUI will result in a fair amount debug messages 145 | being printed that are normally not seen "mymc-gui" is used. 146 | 147 | - It's possible to use mymc create images that are bigger (or 148 | smaller) than standard PS2 memory cards. Be very careful if you 149 | do this, not all games may be compatible with such images. 150 | 151 | - The bad block list on images is ignored. Since memory card 152 | images created with either PCSX2 or mymc won't have any bad 153 | blocks, this shouldn't be a problem unless you've somehow 154 | extracted a complete image from a real memory card and expect to 155 | copy it back. 156 | 157 | - The PS2 only uses at most 8,000 KB of a memory card, but there 158 | is actually 8,135 KB of allocatable space on a standard 159 | error-free memory card. The extra 135 KB is reserved so that 160 | memory card with bad blocks don't appear to have less space than 161 | memory cards with fewer or no bad blocks. Since there are no 162 | bad blocks on memory card images, mymc uses the full capacity 163 | provided by standard memory cards. 164 | 165 | 166 | PYTHON SOURCE DISTRIBUTION 167 | ========================== 168 | 169 | The "source code" distribution of mymc is provided for users of Linux 170 | and other non-Windows operating systems. It uses the same Python code 171 | that the Windows distribution is built with (using py2exe) and 172 | supports all the same functionality. One big difference is that the 173 | Windows DLL "mymcsup.dll" is not included and as a result compressing 174 | and decompressing MAX Drive saves will be as much as 100 times slower. 175 | The GUI mode is hasn't been extensively tested on non-Windows systems, 176 | and the 3D display of save file icons requires the DLL. The Python 177 | source version should support big-endian machines, but this hasn't 178 | been tested. 179 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | # 2 | # gui.py 3 | # 4 | # By Ross Ridge 5 | # Public Domain 6 | # 7 | 8 | """Graphical user-interface for mymc.""" 9 | 10 | _SCCS_ID = "@(#) mymc gui.py 1.8 22/02/05 19:20:59\n" 11 | 12 | import os 13 | import sys 14 | import struct 15 | import cStringIO 16 | import time 17 | from functools import partial 18 | 19 | # Work around a problem with mixing wx and py2exe 20 | if os.name == "nt" and hasattr(sys, "setdefaultencoding"): 21 | sys.setdefaultencoding("mbcs") 22 | import wx 23 | 24 | import ps2mc 25 | import ps2save 26 | import guires 27 | 28 | try: 29 | import ctypes 30 | import mymcicon 31 | D3DXVECTOR3 = mymcicon.D3DXVECTOR3 32 | D3DXVECTOR4 = mymcicon.D3DXVECTOR4 33 | D3DXVECTOR4_ARRAY3 = mymcicon.D3DXVECTOR4_ARRAY3 34 | 35 | def mkvec4arr3(l): 36 | return D3DXVECTOR4_ARRAY3(*[D3DXVECTOR4(*vec) 37 | for vec in l]) 38 | except ImportError: 39 | mymcicon = None 40 | 41 | lighting_none = {"lighting": False, 42 | "vertex_diffuse": False, 43 | "alt_lighting": False, 44 | "light_dirs": [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 45 | "light_colours": [[0, 0, 0, 0], [0, 0, 0, 0], 46 | [0, 0, 0, 0]], 47 | "ambient": [0, 0, 0, 0]} 48 | 49 | lighting_diffuse = {"lighting": False, 50 | "vertex_diffuse": True, 51 | "alt_lighting": False, 52 | "light_dirs": [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 53 | "light_colours": [[0, 0, 0, 0], [0, 0, 0, 0], 54 | [0, 0, 0, 0]], 55 | "ambient": [0, 0, 0, 0]} 56 | 57 | lighting_icon = {"lighting": True, 58 | "vertex_diffuse": True, 59 | "alt_lighting": False, 60 | "light_dirs": [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 61 | "light_colours": [[0, 0, 0, 0], [0, 0, 0, 0], 62 | [0, 0, 0, 0]], 63 | "ambient": [0, 0, 0, 0]} 64 | 65 | lighting_alternate = {"lighting": True, 66 | "vertex_diffuse": True, 67 | "alt_lighting": True, 68 | "light_dirs": [[1, -1, 2, 0], 69 | [-1, 1, -2, 0], 70 | [0, 1, 0, 0]], 71 | "light_colours": [[1, 1, 1, 1], 72 | [1, 1, 1, 1], 73 | [0.7, 0.7, 0.7, 1]], 74 | "ambient": [0.5, 0.5, 0.5, 1]} 75 | 76 | lighting_alternate2 = {"lighting": True, 77 | "vertex_diffuse": False, 78 | "alt_lighting": True, 79 | "light_dirs": [[1, -1, 2, 0], 80 | [-1, 1, -2, 0], 81 | [0, 4, 1, 0]], 82 | "light_colours": [[0.7, 0.7, 0.7, 1], 83 | [0.7, 0.7, 0.7, 1], 84 | [0.2, 0.2, 0.2, 1]], 85 | "ambient": [0.3, 0.3, 0.3, 1]} 86 | 87 | camera_default = [0, 4, -8] 88 | camera_high = [0, 7, -6] 89 | camera_near = [0, 3, -6] 90 | camera_flat = [0, 2, -7.5] 91 | 92 | def get_dialog_units(win): 93 | return win.ConvertDialogToPixels((1, 1))[0] 94 | 95 | def single_title(title): 96 | """Convert the two parts of an icon.sys title into one string.""" 97 | 98 | title = title[0] + " " + title[1] 99 | return u" ".join(title.split()) 100 | 101 | def _get_icon_resource_as_images(name): 102 | ico = guires.resources[name] 103 | images = [] 104 | f = cStringIO.StringIO(ico) 105 | count = struct.unpack("= size[0] and sz[1] >= size[1]: 134 | if ((best_size[0] < size[0] or best_size[1] < size[1]) 135 | or sz[0] * sz[1] < best_size[0] * best_size[1]): 136 | best = img 137 | best_size = sz 138 | elif sz[0] * sz[1] > best_size[0] * best_size[1]: 139 | best = img 140 | best_size = sz 141 | img = best.Rescale(size[0], size[1], wx.IMAGE_QUALITY_HIGH) 142 | return wx.Bitmap(img) 143 | 144 | 145 | class dirlist_control(wx.ListCtrl): 146 | """Lists all the save files in a memory card image.""" 147 | 148 | def __init__(self, parent, evt_focus, evt_select, config): 149 | self.config = config 150 | self.selected = set() 151 | self.evt_select = evt_select 152 | wx.ListCtrl.__init__(self, parent, wx.ID_ANY, 153 | style = wx.LC_REPORT) 154 | self.Bind(wx.EVT_LIST_COL_CLICK, self.evt_col_click) 155 | self.Bind(wx.EVT_LIST_ITEM_FOCUSED, evt_focus) 156 | self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.evt_item_selected) 157 | self.Bind(wx.EVT_LIST_ITEM_DESELECTED, 158 | self.evt_item_deselected) 159 | 160 | def _update_dirtable(self, mc, dir): 161 | self.dirtable = table = [] 162 | enc = "unicode" 163 | if self.config.get_ascii(): 164 | enc = "ascii" 165 | for ent in dir: 166 | if not ps2mc.mode_is_dir(ent[0]): 167 | continue 168 | dirname = "/" + ent[8] 169 | s = mc.get_icon_sys(dirname) 170 | if s == None: 171 | continue 172 | a = ps2save.unpack_icon_sys(s) 173 | size = mc.dir_size(dirname) 174 | title = ps2save.icon_sys_title(a, encoding = enc) 175 | table.append((ent, s, size, title)) 176 | 177 | def update_dirtable(self, mc): 178 | self.dirtable = [] 179 | if mc == None: 180 | return 181 | dir = mc.dir_open("/") 182 | try: 183 | self._update_dirtable(mc, dir) 184 | finally: 185 | dir.close() 186 | 187 | def get_dir_name(self, i): 188 | return self.dirtable[i][0][8] 189 | 190 | def get_dir_title(self, i): 191 | return self.dirtable[i][3] 192 | 193 | def get_dir_size(self, i): 194 | return self.dirtable[i][2] 195 | 196 | def get_dir_modified(self, i): 197 | m = list(self.dirtable[i][0][6]) 198 | m.reverse() 199 | return m 200 | 201 | def sort_items(self, key): 202 | def cmp(i1, i2): 203 | a1 = key(i1) 204 | a2 = key(i2) 205 | if a1 < a2: 206 | return -1 207 | if a1 > a2: 208 | return 1 209 | return 0 210 | self.SortItems(cmp) 211 | 212 | def evt_col_click(self, event): 213 | col = event.GetColumn() 214 | if col == 0: 215 | key = self.get_dir_name 216 | elif col == 1: 217 | key = self.get_dir_size 218 | elif col == 2: 219 | key = self.get_dir_modified 220 | elif col == 3: 221 | key = self.get_dir_title 222 | self.sort_items(key) 223 | return 224 | 225 | def evt_item_selected(self, event): 226 | self.selected.add(event.GetData()) 227 | self.evt_select(event) 228 | 229 | def evt_item_deselected(self, event): 230 | self.selected.discard(event.GetData()) 231 | self.evt_select(event) 232 | 233 | def update(self, mc): 234 | """Update the ListCtrl according to the contents of the 235 | memory card image.""" 236 | 237 | self.ClearAll() 238 | self.selected = set() 239 | self.InsertColumn(0, "Directory") 240 | self.InsertColumn(1, "Size") 241 | self.InsertColumn(2, "Modified") 242 | self.InsertColumn(3, "Description") 243 | li = self.GetColumn(1) 244 | li.SetAlign(wx.LIST_FORMAT_RIGHT) 245 | li.SetText("Size") 246 | self.SetColumn(1, li) 247 | 248 | self.update_dirtable(mc) 249 | 250 | empty = (len(self.dirtable) == 0) 251 | self.Enable(not empty) 252 | if empty: 253 | return 254 | 255 | for (i, a) in enumerate(self.dirtable): 256 | (ent, icon_sys, size, title) = a 257 | li = self.InsertItem(i, ent[8]) 258 | self.SetItem(li, 1, "%dK" % (size / 1024)) 259 | m = ent[6] 260 | self.SetItem(li, 2, ("%04d-%02d-%02d %02d:%02d" 261 | % (m[5], m[4], m[3], m[2], m[1]))) 262 | self.SetItem(li, 3, single_title(title)) 263 | self.SetItemData(li, i) 264 | 265 | du = get_dialog_units(self) 266 | for i in range(4): 267 | self.SetColumnWidth(i, wx.LIST_AUTOSIZE) 268 | self.SetColumnWidth(i, self.GetColumnWidth(i) + du) 269 | self.sort_items(self.get_dir_name) 270 | 271 | 272 | class icon_window(wx.Window): 273 | """Displays a save file's 3D icon. Windows only. 274 | 275 | The rendering of the 3D icon is handled by C++ code in the 276 | mymcicon DLL which subclasses this window. This class mainly 277 | handles configuration options that affect how the 3D icon is 278 | displayed. 279 | """ 280 | 281 | ID_CMD_ANIMATE = 201 282 | ID_CMD_LIGHT_NONE = 202 283 | ID_CMD_LIGHT_ICON = 203 284 | ID_CMD_LIGHT_ALT1 = 204 285 | ID_CMD_LIGHT_ALT2 = 205 286 | ID_CMD_CAMERA_FLAT = 206 287 | ID_CMD_CAMERA_DEFAULT = 207 288 | ID_CMD_CAMERA_NEAR = 209 289 | ID_CMD_CAMERA_HIGH = 210 290 | 291 | light_options = {ID_CMD_LIGHT_NONE: lighting_none, 292 | ID_CMD_LIGHT_ICON: lighting_icon, 293 | ID_CMD_LIGHT_ALT1: lighting_alternate, 294 | ID_CMD_LIGHT_ALT2: lighting_alternate2} 295 | 296 | camera_options = {ID_CMD_CAMERA_FLAT: camera_flat, 297 | ID_CMD_CAMERA_DEFAULT: camera_default, 298 | ID_CMD_CAMERA_NEAR: camera_near, 299 | ID_CMD_CAMERA_HIGH: camera_high} 300 | 301 | def append_menu_options(self, win, menu): 302 | menu.AppendCheckItem(icon_window.ID_CMD_ANIMATE, 303 | "Animate Icons") 304 | menu.AppendSeparator() 305 | menu.AppendRadioItem(icon_window.ID_CMD_LIGHT_NONE, 306 | "Lighting Off") 307 | menu.AppendRadioItem(icon_window.ID_CMD_LIGHT_ICON, 308 | "Icon Lighting") 309 | menu.AppendRadioItem(icon_window.ID_CMD_LIGHT_ALT1, 310 | "Alternate Lighting") 311 | menu.AppendRadioItem(icon_window.ID_CMD_LIGHT_ALT2, 312 | "Alternate Lighting 2") 313 | menu.AppendSeparator() 314 | menu.AppendRadioItem(icon_window.ID_CMD_CAMERA_FLAT, 315 | "Camera Flat") 316 | menu.AppendRadioItem(icon_window.ID_CMD_CAMERA_DEFAULT, 317 | "Camera Default") 318 | menu.AppendRadioItem(icon_window.ID_CMD_CAMERA_NEAR, 319 | "Camera Near") 320 | menu.AppendRadioItem(icon_window.ID_CMD_CAMERA_HIGH, 321 | "Camera High") 322 | 323 | bind_menu = partial(win.Bind, wx.EVT_MENU) 324 | 325 | bind_menu(self.evt_menu_animate, None, 326 | icon_window.ID_CMD_ANIMATE) 327 | 328 | bind_menu_light = partial(bind_menu, self.evt_menu_light, None) 329 | bind_menu_light(icon_window.ID_CMD_LIGHT_NONE) 330 | bind_menu_light(icon_window.ID_CMD_LIGHT_ICON) 331 | bind_menu_light(icon_window.ID_CMD_LIGHT_ALT1) 332 | bind_menu_light(icon_window.ID_CMD_LIGHT_ALT2) 333 | 334 | bind_menu_camera = partial(bind_menu, 335 | self.evt_menu_camera, None) 336 | bind_menu_camera(icon_window.ID_CMD_CAMERA_FLAT) 337 | bind_menu_camera(icon_window.ID_CMD_CAMERA_DEFAULT) 338 | bind_menu_camera(icon_window.ID_CMD_CAMERA_NEAR) 339 | bind_menu_camera(icon_window.ID_CMD_CAMERA_HIGH) 340 | 341 | def __init__(self, parent, focus): 342 | self.failed = False 343 | wx.Window.__init__(self, parent) 344 | if mymcicon == None: 345 | self.failed = True 346 | return 347 | r = mymcicon.init_icon_renderer(focus.GetHandle(), 348 | self.GetHandle()) 349 | if r == -1: 350 | print "init_icon_renderer failed" 351 | self.failed = True 352 | return 353 | 354 | self.config = config = mymcicon.icon_config() 355 | config.animate = True 356 | 357 | self.menu = wx.Menu() 358 | self.append_menu_options(self, self.menu) 359 | self.set_lighting(self.ID_CMD_LIGHT_ALT2) 360 | self.set_camera(self.ID_CMD_CAMERA_DEFAULT) 361 | 362 | self.Bind(wx.EVT_CONTEXT_MENU, self.evt_context_menu) 363 | 364 | def __del__(self): 365 | if mymcicon != None: 366 | mymcicon.delete_icon_renderer() 367 | 368 | def update_menu(self, menu): 369 | """Update the content menu according to the current config.""" 370 | 371 | menu.Check(icon_window.ID_CMD_ANIMATE, self.config.animate) 372 | menu.Check(self.lighting_id, True) 373 | menu.Check(self.camera_id, True) 374 | 375 | def load_icon(self, icon_sys, icon): 376 | """Pass the raw icon data to the support DLL for display.""" 377 | 378 | if self.failed: 379 | return 380 | 381 | if icon_sys == None or icon == None: 382 | r = mymcicon.load_icon(None, 0, None, 0) 383 | else: 384 | r = mymcicon.load_icon(icon_sys, len(icon_sys), 385 | icon, len(icon)) 386 | if r != 0: 387 | print "load_icon", r 388 | self.failed = True 389 | 390 | def _set_lighting(self, lighting, vertex_diffuse, alt_lighting, 391 | light_dirs, light_colours, ambient): 392 | if self.failed: 393 | return 394 | config = self.config 395 | config.lighting = lighting 396 | config.vertex_diffuse = vertex_diffuse 397 | config.alt_lighting = alt_lighting 398 | config.light_dirs = mkvec4arr3(light_dirs) 399 | config.light_colours = mkvec4arr3(light_colours) 400 | config.ambient = D3DXVECTOR4(*ambient) 401 | if mymcicon.set_config(config) == -1: 402 | self.failed = True 403 | 404 | def set_lighting(self, id): 405 | self.lighting_id = id 406 | self._set_lighting(**self.light_options[id]) 407 | 408 | def set_animate(self, animate): 409 | if self.failed: 410 | return 411 | self.config.animate = animate 412 | if mymcicon.set_config(self.config) == -1: 413 | self.failed = True 414 | 415 | def _set_camera(self, camera): 416 | if self.failed: 417 | return 418 | self.config.camera = mymcicon.D3DXVECTOR3(*camera) 419 | if mymcicon.set_config(self.config) == -1: 420 | self.failed = True 421 | 422 | def set_camera(self, id): 423 | self.camera_id = id 424 | self._set_camera(self.camera_options[id]) 425 | 426 | def evt_context_menu(self, event): 427 | self.update_menu(self.menu) 428 | self.PopupMenu(self.menu) 429 | 430 | def evt_menu_animate(self, event): 431 | self.set_animate(not self.config.animate) 432 | 433 | def evt_menu_light(self, event): 434 | self.set_lighting(event.GetId()) 435 | 436 | def evt_menu_camera(self, event): 437 | self.set_camera(event.GetId()) 438 | 439 | class gui_config(wx.Config): 440 | """A class for holding the persistant configuration state.""" 441 | 442 | memcard_dir = "Memory Card Directory" 443 | savefile_dir = "Save File Directory" 444 | ascii = "ASCII Descriptions" 445 | 446 | def __init__(self): 447 | wx.Config.__init__(self, "mymc", "Ross Ridge", 448 | style = wx.CONFIG_USE_LOCAL_FILE) 449 | 450 | def get_memcard_dir(self, default = None): 451 | return self.Read(gui_config.memcard_dir, default) 452 | 453 | def set_memcard_dir(self, value): 454 | return self.Write(gui_config.memcard_dir, value) 455 | 456 | def get_savefile_dir(self, default = None): 457 | return self.Read(gui_config.savefile_dir, default) 458 | 459 | def set_savefile_dir(self, value): 460 | return self.Write(gui_config.savefile_dir, value) 461 | 462 | def get_ascii(self, default = False): 463 | return bool(self.ReadInt(gui_config.ascii, int(bool(default)))) 464 | 465 | def set_ascii(self, value): 466 | return self.WriteInt(gui_config.ascii, int(bool(value))) 467 | 468 | def add_tool(toolbar, id, label, ico): 469 | tbsize = toolbar.GetToolBitmapSize() 470 | bmp = get_icon_resource_bmp(ico, tbsize) 471 | return toolbar.AddTool(id, label, bmp, shortHelp = label) 472 | 473 | class gui_frame(wx.Frame): 474 | """The main top level window.""" 475 | 476 | ID_CMD_EXIT = wx.ID_EXIT 477 | ID_CMD_OPEN = wx.ID_OPEN 478 | ID_CMD_EXPORT = 103 479 | ID_CMD_IMPORT = 104 480 | ID_CMD_DELETE = wx.ID_DELETE 481 | ID_CMD_ASCII = 106 482 | 483 | def message_box(self, message, caption = "mymc", style = wx.OK, 484 | x = -1, y = -1): 485 | return wx.MessageBox(message, caption, style, self, x, y) 486 | 487 | def error_box(self, msg): 488 | return self.message_box(msg, "Error", wx.OK | wx.ICON_ERROR) 489 | 490 | def mc_error(self, value, filename = None): 491 | """Display a message box for EnvironmentError exeception.""" 492 | 493 | if filename == None: 494 | filename = getattr(value, "filename") 495 | if filename == None: 496 | filename = self.mcname 497 | if filename == None: 498 | filename = "???" 499 | 500 | strerror = getattr(value, "strerror", None) 501 | if strerror == None: 502 | strerror = "unknown error" 503 | 504 | return self.error_box(filename + ": " + strerror) 505 | 506 | def __init__(self, parent, title, mcname = None): 507 | self.f = None 508 | self.mc = None 509 | self.mcname = None 510 | self.icon_win = None 511 | 512 | size = (750, 350) 513 | if mymcicon == None: 514 | size = (500, 350) 515 | wx.Frame.__init__(self, parent, wx.ID_ANY, title, size = size) 516 | 517 | self.Bind(wx.EVT_CLOSE, self.evt_close) 518 | 519 | self.config = gui_config() 520 | self.title = title 521 | 522 | self.SetIcons(get_icon_resource("mc4.ico")) 523 | 524 | bind_menu = (lambda handler, id: 525 | self.Bind(wx.EVT_MENU, handler, None, id)) 526 | bind_menu(self.evt_cmd_exit, self.ID_CMD_EXIT) 527 | bind_menu(self.evt_cmd_open, self.ID_CMD_OPEN) 528 | bind_menu(self.evt_cmd_export, self.ID_CMD_EXPORT) 529 | bind_menu(self.evt_cmd_import, self.ID_CMD_IMPORT) 530 | bind_menu(self.evt_cmd_delete, self.ID_CMD_DELETE) 531 | bind_menu(self.evt_cmd_ascii, self.ID_CMD_ASCII, ) 532 | 533 | filemenu = wx.Menu() 534 | filemenu.Append(self.ID_CMD_OPEN, "&Open...", 535 | "Opens an existing PS2 memory card image.") 536 | filemenu.AppendSeparator() 537 | self.export_menu_item = filemenu.Append( 538 | self.ID_CMD_EXPORT, "&Export...", 539 | "Export a save file from this image.") 540 | self.import_menu_item = filemenu.Append( 541 | self.ID_CMD_IMPORT, "&Import...", 542 | "Import a save file into this image.") 543 | self.delete_menu_item = filemenu.Append( 544 | self.ID_CMD_DELETE, "&Delete") 545 | filemenu.AppendSeparator() 546 | filemenu.Append(self.ID_CMD_EXIT, "E&xit") 547 | 548 | optionmenu = wx.Menu() 549 | self.ascii_menu_item = optionmenu.AppendCheckItem( 550 | self.ID_CMD_ASCII, "&ASCII Descriptions", 551 | "Show descriptions in ASCII instead of Shift-JIS") 552 | 553 | 554 | self.Bind(wx.EVT_MENU_OPEN, self.evt_menu_open); 555 | 556 | self.CreateToolBar(wx.TB_HORIZONTAL) 557 | self.toolbar = toolbar = self.GetToolBar() 558 | tbsize = (32, 32) 559 | toolbar.SetToolBitmapSize(tbsize) 560 | add_tool(toolbar, self.ID_CMD_OPEN, "Open", "mc2.ico") 561 | toolbar.AddSeparator() 562 | add_tool(toolbar, self.ID_CMD_IMPORT, "Import", "mc5b.ico") 563 | add_tool(toolbar, self.ID_CMD_EXPORT, "Export", "mc6a.ico") 564 | toolbar.Realize() 565 | 566 | self.statusbar = self.CreateStatusBar(2, 567 | style = wx.STB_SIZEGRIP) 568 | self.statusbar.SetStatusWidths([-2, -1]) 569 | 570 | panel = wx.Panel(self, wx.ID_ANY, (0, 0)) 571 | 572 | self.dirlist = dirlist_control(panel, 573 | self.evt_dirlist_item_focused, 574 | self.evt_dirlist_select, 575 | self.config) 576 | if mcname != None: 577 | self.open_mc(mcname) 578 | else: 579 | self.refresh() 580 | 581 | sizer = wx.BoxSizer(wx.HORIZONTAL) 582 | sizer.Add(self.dirlist, 2, wx.EXPAND) 583 | sizer.AddSpacer(5) 584 | 585 | icon_win = None 586 | if mymcicon != None: 587 | icon_win = icon_window(panel, self) 588 | if icon_win.failed: 589 | icon_win.Destroy() 590 | icon_win = None 591 | self.icon_win = icon_win 592 | 593 | if icon_win == None: 594 | self.info1 = None 595 | self.info2 = None 596 | else: 597 | self.icon_menu = icon_menu = wx.Menu() 598 | icon_win.append_menu_options(self, icon_menu) 599 | optionmenu.AppendSubMenu(icon_menu, "Icon Window") 600 | title_style = wx.ALIGN_RIGHT | wx.ST_NO_AUTORESIZE 601 | 602 | self.info1 = wx.StaticText(panel, -1, "", 603 | style = title_style) 604 | self.info2 = wx.StaticText(panel, -1, "", 605 | style = title_style) 606 | # self.info3 = wx.StaticText(panel, -1, "") 607 | 608 | info_sizer = wx.BoxSizer(wx.VERTICAL) 609 | info_sizer.Add(self.info1, 0, wx.EXPAND) 610 | info_sizer.Add(self.info2, 0, wx.EXPAND) 611 | # info_sizer.Add(self.info3, 0, wx.EXPAND) 612 | info_sizer.AddSpacer(5) 613 | info_sizer.Add(icon_win, 1, wx.EXPAND) 614 | 615 | sizer.Add(info_sizer, 1, wx.EXPAND | wx.ALL, 616 | border = 5) 617 | 618 | menubar = wx.MenuBar() 619 | menubar.Append(filemenu, "&File") 620 | menubar.Append(optionmenu, "&Options") 621 | self.SetMenuBar(menubar) 622 | 623 | 624 | panel.SetSizer(sizer) 625 | panel.SetAutoLayout(True) 626 | sizer.Fit(panel) 627 | 628 | self.Show(True) 629 | 630 | if self.mc == None: 631 | self.evt_cmd_open() 632 | 633 | def _close_mc(self): 634 | if self.mc != None: 635 | try: 636 | self.mc.close() 637 | except EnvironmentError, value: 638 | self.mc_error(value) 639 | self.mc = None 640 | if self.f != None: 641 | try: 642 | self.f.close() 643 | except EnvironmentError, value: 644 | self.mc_error(value) 645 | self.f = None 646 | self.mcname = None 647 | 648 | def refresh(self): 649 | try: 650 | self.dirlist.update(self.mc) 651 | except EnvironmentError, value: 652 | self.mc_error(value) 653 | self._close_mc() 654 | self.dirlist.update(None) 655 | 656 | mc = self.mc 657 | 658 | self.toolbar.EnableTool(self.ID_CMD_IMPORT, mc != None) 659 | self.toolbar.EnableTool(self.ID_CMD_EXPORT, False) 660 | 661 | if mc == None: 662 | status = "No memory card image" 663 | else: 664 | free = mc.get_free_space() / 1024 665 | limit = mc.get_allocatable_space() / 1024 666 | status = "%dK of %dK free" % (free, limit) 667 | self.statusbar.SetStatusText(status, 1) 668 | 669 | def open_mc(self, filename): 670 | self._close_mc() 671 | self.statusbar.SetStatusText("", 1) 672 | if self.icon_win != None: 673 | self.icon_win.load_icon(None, None) 674 | 675 | f = None 676 | try: 677 | f = file(filename, "r+b") 678 | mc = ps2mc.ps2mc(f) 679 | except EnvironmentError, value: 680 | if f != None: 681 | f.close() 682 | self.mc_error(value, filename) 683 | self.SetTitle(self.title) 684 | self.refresh() 685 | return 686 | 687 | self.f = f 688 | self.mc = mc 689 | self.mcname = filename 690 | self.SetTitle(filename + " - " + self.title) 691 | self.refresh() 692 | 693 | def evt_menu_open(self, event): 694 | self.import_menu_item.Enable(self.mc != None) 695 | selected = self.mc != None and len(self.dirlist.selected) > 0 696 | self.export_menu_item.Enable(selected) 697 | self.delete_menu_item.Enable(selected) 698 | self.ascii_menu_item.Check(self.config.get_ascii()) 699 | if self.icon_win != None: 700 | self.icon_win.update_menu(self.icon_menu) 701 | 702 | def evt_dirlist_item_focused(self, event): 703 | if self.icon_win == None: 704 | return 705 | 706 | mc = self.mc 707 | 708 | i = event.GetData() 709 | (ent, icon_sys, size, title) = self.dirlist.dirtable[i] 710 | self.info1.SetLabel(title[0]) 711 | self.info2.SetLabel(title[1]) 712 | 713 | a = ps2save.unpack_icon_sys(icon_sys) 714 | try: 715 | mc.chdir("/" + ent[8]) 716 | f = mc.open(a[15], "rb") 717 | try: 718 | icon = f.read() 719 | finally: 720 | f.close() 721 | except EnvironmentError, value: 722 | print "icon failed to load", value 723 | self.icon_win.load_icon(None, None) 724 | return 725 | 726 | self.icon_win.load_icon(icon_sys, icon) 727 | 728 | def evt_dirlist_select(self, event): 729 | self.toolbar.EnableTool(self.ID_CMD_IMPORT, self.mc != None) 730 | self.toolbar.EnableTool(self.ID_CMD_EXPORT, 731 | len(self.dirlist.selected) > 0) 732 | 733 | def evt_cmd_open(self, event = None): 734 | fn = wx.FileSelector("Open Memory Card Image", 735 | self.config.get_memcard_dir(""), 736 | "Mcd001.ps2", "ps2", "*.ps2", 737 | wx.FD_FILE_MUST_EXIST | wx.FD_OPEN, 738 | self) 739 | if fn == "": 740 | return 741 | self.open_mc(fn) 742 | if self.mc != None: 743 | dirname = os.path.dirname(fn) 744 | if os.path.isabs(dirname): 745 | self.config.set_memcard_dir(dirname) 746 | 747 | def evt_cmd_export(self, event): 748 | mc = self.mc 749 | if mc == None: 750 | return 751 | 752 | selected = self.dirlist.selected 753 | dirtable = self.dirlist.dirtable 754 | sfiles = [] 755 | for i in selected: 756 | dirname = dirtable[i][0][8] 757 | try: 758 | sf = mc.export_save_file("/" + dirname) 759 | longname = ps2save.make_longname(dirname, sf) 760 | sfiles.append((dirname, sf, longname)) 761 | except EnvironmentError, value: 762 | self.mc_error(value. dirname) 763 | 764 | if len(sfiles) == 0: 765 | return 766 | 767 | dir = self.config.get_savefile_dir("") 768 | if len(selected) == 1: 769 | (dirname, sf, longname) = sfiles[0] 770 | fn = wx.FileSelector("Export " + dirname, 771 | dir, longname, "psu", 772 | "EMS save file (.psu)|*.psu" 773 | "|MAXDrive save file (.max)" 774 | "|*.max", 775 | (wx.FD_OVERWRITE_PROMPT 776 | | wx.FD_SAVE), 777 | self) 778 | if fn == "": 779 | return 780 | try: 781 | f = file(fn, "wb") 782 | try: 783 | if fn.endswith(".max"): 784 | sf.save_max_drive(f) 785 | else: 786 | sf.save_ems(f) 787 | finally: 788 | f.close() 789 | except EnvironmentError, value: 790 | self.mc_error(value, fn) 791 | return 792 | 793 | dir = os.path.dirname(fn) 794 | if os.path.isabs(dir): 795 | self.config.set_savefile_dir(dir) 796 | 797 | self.message_box("Exported " + fn + " successfully.") 798 | return 799 | 800 | dir = wx.DirSelector("Export Save Files", dir, parent = self) 801 | if dir == "": 802 | return 803 | count = 0 804 | for (dirname, sf, longname) in sfiles: 805 | fn = os.path.join(dir, longname) + ".psu" 806 | try: 807 | f = file(fn, "wb") 808 | sf.save_ems(f) 809 | f.close() 810 | count += 1 811 | except EnvironmentError, value: 812 | self.mc_error(value, fn) 813 | if count > 0: 814 | if os.path.isabs(dir): 815 | self.config.set_savefile_dir(dir) 816 | self.message_box("Exported %d file(s) successfully." 817 | % count) 818 | 819 | 820 | def _do_import(self, fn): 821 | sf = ps2save.ps2_save_file() 822 | f = file(fn, "rb") 823 | try: 824 | ft = ps2save.detect_file_type(f) 825 | f.seek(0) 826 | if ft == "max": 827 | sf.load_max_drive(f) 828 | elif ft == "psu": 829 | sf.load_ems(f) 830 | elif ft == "cbs": 831 | sf.load_codebreaker(f) 832 | elif ft == "sps": 833 | sf.load_sharkport(f) 834 | elif ft == "npo": 835 | self.error_box(fn + ": nPort saves" 836 | " are not supported.") 837 | return 838 | else: 839 | self.error_box(fn + ": Save file format not" 840 | " recognized.") 841 | return 842 | finally: 843 | f.close() 844 | 845 | if not self.mc.import_save_file(sf, True): 846 | self.error_box(fn + ": Save file already present.") 847 | 848 | def evt_cmd_import(self, event): 849 | if self.mc == None: 850 | return 851 | 852 | dir = self.config.get_savefile_dir("") 853 | fd = wx.FileDialog(self, "Import Save File", dir, 854 | wildcard = ("PS2 save files" 855 | " (.cbs;.psu;.max;.sps;.xps)" 856 | "|*.cbs;*.psu;*.max;*.sps;*.xps" 857 | "|All files|*.*"), 858 | style = (wx.FD_OPEN | wx.FD_MULTIPLE 859 | | wx.FD_FILE_MUST_EXIST)) 860 | if fd == None: 861 | return 862 | r = fd.ShowModal() 863 | if r == wx.ID_CANCEL: 864 | return 865 | 866 | success = None 867 | for fn in fd.GetPaths(): 868 | try: 869 | self._do_import(fn) 870 | success = fn 871 | except EnvironmentError, value: 872 | self.mc_error(value, fn) 873 | 874 | if success != None: 875 | dir = os.path.dirname(success) 876 | if os.path.isabs(dir): 877 | self.config.set_savefile_dir(dir) 878 | self.refresh() 879 | 880 | def evt_cmd_delete(self, event): 881 | mc = self.mc 882 | if mc == None: 883 | return 884 | 885 | selected = self.dirlist.selected 886 | dirtable = self.dirlist.dirtable 887 | 888 | dirnames = [dirtable[i][0][8] 889 | for i in selected] 890 | if len(selected) == 1: 891 | title = dirtable[list(selected)[0]][3] 892 | s = dirnames[0] + " (" + single_title(title) + ")" 893 | else: 894 | s = ", ".join(dirnames) 895 | if len(s) > 200: 896 | s = s[:200] + "..." 897 | r = self.message_box("Are you sure you want to delete " 898 | + s + "?", 899 | "Delete Save File Confirmation", 900 | wx.YES_NO) 901 | if r != wx.YES: 902 | return 903 | 904 | for dn in dirnames: 905 | try: 906 | mc.rmdir("/" + dn) 907 | except EnvironmentError, value: 908 | self.mc_error(value, dn) 909 | 910 | mc.check() 911 | self.refresh() 912 | 913 | def evt_cmd_ascii(self, event): 914 | self.config.set_ascii(not self.config.get_ascii()) 915 | self.refresh() 916 | 917 | def evt_cmd_exit(self, event): 918 | self.Close(True) 919 | 920 | def evt_close(self, event): 921 | self._close_mc() 922 | self.Destroy() 923 | 924 | def run(filename = None): 925 | """Display a GUI for working with memory card images.""" 926 | 927 | wx_app = wx.App() 928 | frame = gui_frame(None, "mymc", filename) 929 | return wx_app.MainLoop() 930 | 931 | if __name__ == "__main__": 932 | import gc 933 | gc.set_debug(gc.DEBUG_LEAK) 934 | 935 | run("test.ps2") 936 | 937 | gc.collect() 938 | for o in gc.garbage: 939 | print 940 | print o 941 | if type(o) == ps2mc.ps2mc_file: 942 | for m in dir(o): 943 | print m, getattr(o, m) 944 | 945 | 946 | # while True: 947 | # for o in gc.garbage: 948 | # if type(o) == ps2mc.ps2mc_file: 949 | # for m in dir(o): 950 | # if getattr(o, m) == None: 951 | # continue 952 | # if (m == "__del__" 953 | # or m == "__class__" 954 | # or m == "__dict__" 955 | # or m == "__weakref__"): 956 | # continue 957 | # print m 958 | # setattr(o, m, None) 959 | # o = None 960 | # break 961 | # break 962 | # del gc.garbage[:] 963 | # gc.collect() 964 | -------------------------------------------------------------------------------- /guires.py: -------------------------------------------------------------------------------- 1 | resources = { 2 | "mc4.ico": ( 3 | "AAABAAIAICAQAAAAAADoAgAAJgAAADAwAAEAAAAAqA4AAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAA\n" 4 | "AIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAICAgADAwMAA\n" 5 | "AAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 6 | "AAAAAAAAAAcABwAAAAAAAAAAAAAAAABwAABwAAAAAAAAAAAAAAAHAAYAB3d3d3dwAAAAAAAAcABm\n" 7 | "YAB3d3dwAAAAAAAABwAGZmYAB3d3AAAAAAAAAHAAZmZmYAB4iIiIiIiAAAcABmZmZgAAB/+P+q/4\n" 8 | "gABwAGZmZmAAAABu7u7u6IAHAAZmZmYAAAAABu7u7u6AAABmZmZgBwcAAABu7u7ugAAGZmZmAHBw\n" 9 | "AAAABu7u7oAAAGZmYAcHBwAAAABu7u6ABwAGZgBwcHBwAAAABu7ugAAAAGAABwcHAABAAADu7oAA\n" 10 | "AAAAAHB3cAAEAAAG7u6AAAAAAAAAB3AAwAAAbu7ugAAABwAAAAAABAAABu7u7oAAAAhgAAAAAMAH\n" 11 | "AG7u7u6AAAAI5gAAAAAAdwbu7u7ugAAACO5gAAxAAABu7u7u7oAAAAju5gAEzAAG7u7u7u6AAAAI\n" 12 | "7u5gAAwAbu7u7u7ugAAACO7u5gAABu7u7u7u7oAAAAju7u5gAG7u7u7u7u6AAAAI7u7u5gbu7u7u\n" 13 | "7u7ugAAACI7u7u7u7u7u7u7u6IAAAAiIiIiIiIiIiIiIiIiAAAAIiIiIiIiIiIiIiIiIgAAAAAAA\n" 14 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////g////wH///4AAH/8AAH/+AAD//AA\n" 15 | "AAHgAAABwAAAAYAAAAGAAAABgAAAAYAAAAGAAAAB4AAAAfAAAAH4AAAB+AAAAfgAAAH4AAAB+AAA\n" 16 | "AfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB//////////8oAAAAMAAAAGAAAAABAAgA\n" 17 | "AAAAAIAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwADA\n" 18 | "3MAA8MqmAAAgQAAAIGAAACCAAAAgoAAAIMAAACDgAABAAAAAQCAAAEBAAABAYAAAQIAAAECgAABA\n" 19 | "wAAAQOAAAGAAAABgIAAAYEAAAGBgAABggAAAYKAAAGDAAABg4AAAgAAAAIAgAACAQAAAgGAAAICA\n" 20 | "AACAoAAAgMAAAIDgAACgAAAAoCAAAKBAAACgYAAAoIAAAKCgAACgwAAAoOAAAMAAAADAIAAAwEAA\n" 21 | "AMBgAADAgAAAwKAAAMDAAADA4AAA4AAAAOAgAADgQAAA4GAAAOCAAADgoAAA4MAAAODgAEAAAABA\n" 22 | "ACAAQABAAEAAYABAAIAAQACgAEAAwABAAOAAQCAAAEAgIABAIEAAQCBgAEAggABAIKAAQCDAAEAg\n" 23 | "4ABAQAAAQEAgAEBAQABAQGAAQECAAEBAoABAQMAAQEDgAEBgAABAYCAAQGBAAEBgYABAYIAAQGCg\n" 24 | "AEBgwABAYOAAQIAAAECAIABAgEAAQIBgAECAgABAgKAAQIDAAECA4ABAoAAAQKAgAECgQABAoGAA\n" 25 | "QKCAAECgoABAoMAAQKDgAEDAAABAwCAAQMBAAEDAYABAwIAAQMCgAEDAwABAwOAAQOAAAEDgIABA\n" 26 | "4EAAQOBgAEDggABA4KAAQODAAEDg4ACAAAAAgAAgAIAAQACAAGAAgACAAIAAoACAAMAAgADgAIAg\n" 27 | "AACAICAAgCBAAIAgYACAIIAAgCCgAIAgwACAIOAAgEAAAIBAIACAQEAAgEBgAIBAgACAQKAAgEDA\n" 28 | "AIBA4ACAYAAAgGAgAIBgQACAYGAAgGCAAIBgoACAYMAAgGDgAICAAACAgCAAgIBAAICAYACAgIAA\n" 29 | "gICgAICAwACAgOAAgKAAAICgIACAoEAAgKBgAICggACAoKAAgKDAAICg4ACAwAAAgMAgAIDAQACA\n" 30 | "wGAAgMCAAIDAoACAwMAAgMDgAIDgAACA4CAAgOBAAIDgYACA4IAAgOCgAIDgwACA4OAAwAAAAMAA\n" 31 | "IADAAEAAwABgAMAAgADAAKAAwADAAMAA4ADAIAAAwCAgAMAgQADAIGAAwCCAAMAgoADAIMAAwCDg\n" 32 | "AMBAAADAQCAAwEBAAMBAYADAQIAAwECgAMBAwADAQOAAwGAAAMBgIADAYEAAwGBgAMBggADAYKAA\n" 33 | "wGDAAMBg4ADAgAAAwIAgAMCAQADAgGAAwICAAMCAoADAgMAAwIDgAMCgAADAoCAAwKBAAMCgYADA\n" 34 | "oIAAwKCgAMCgwADAoOAAwMAAAMDAIADAwEAAwMBgAMDAgADAwKAA8Pv/AKSgoACAgIAAAAD/AAD/\n" 35 | "AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 36 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 37 | "AAAAAAAAAAAAAAAAAAAAAAAAAKQAAKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 38 | "AAAAAAAAAAAApAAAAACkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACk\n" 39 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKQAAAAAAAAApAAA\n" 40 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApAAAAAAACQAAAACkpKSkpKSkpKSk\n" 41 | "pKSkpAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkAAAAAAAJCQkAAACkpKSkpKSkpKSkpKQAAAAAAAAA\n" 42 | "AAAAAAAAAAAAAAAAAAAAAKQAAAAAAAkJCQkJAAAAAKSkpKSkpKQAAAAAAAAAAAAAAAAAAAAAAAAA\n" 43 | "AAAAAAAApAAAAAAACQkJCQkJCQAAAACkpKSkpKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkAAAA\n" 44 | "AAAJCQkJCQkJCQkAAAAApPf39/f39/f39/f39/f39/f39/cAAAAAAAAAAKQAAAAAAAkJCQkJCQkJ\n" 45 | "CQkJAAAAAKQHBwf3BwcH9/f39wcHB/f39/cAAAAAAAAApAAAAAAACQkJCQkJCQkJCQkAAAAAAACk\n" 46 | "9vb39vb29/r69/b29vf39/cAAAAAAACkAAAAAAAJCQkJCQkJCQkJCQAAAAAAAAAApPf39/f39/f3\n" 47 | "9/f39/f39/cAAAAAAKQAAAAAAAkJCQkJCQkJCQkJAAAAAAAAAAAAAKT39/f39/f39/f39/f39/cA\n" 48 | "AAAApAAAAAAACQkJCQkJCQkJCQkAAAAAAAAAAAAAAAD3/v7+/v7+/v7+/v739/cAAACkAAAAAAAJ\n" 49 | "CQkJCQkJCQkJCQAApAAApAAAAAAAAAAAB/7+/v7+/v7+/v7+9/cAAAAAAAAAAAkJCQkJCQkJCQkJ\n" 50 | "AACkAACkAAAAAAAAAAAAAAf+/v7+/v7+/v7+9/cAAAAAAAAACQkJCQkJCQkJCQkAAKQAAKQAAAAA\n" 51 | "AAAAAAAAAAAH/v7+/v7+/v7+9/cAAACkAAAAAAkJCQkJCQkJCQAApAAApAAAAKQAAAAAAAAAAAAA\n" 52 | "B/7+/v7+/v7+9/cAAAAApAAAAAAJCQkJCQkJAACkAACkAAAApACkAAAAAAAAAAAAAAf+/v7+/v7+\n" 53 | "9/cAAAAAAACkAAAACQkJCQkAAKQAAKQAAACkAKQAAAAAAAAAAAAAAAAH/v7+/v7+9/cAAAAAAAAA\n" 54 | "AAAAAAkJCQAAAAAApAAAAAAApAAAAAAAAADAAAAAAAAAB/7+/v7+9/cAAAAAAAAAAKQAAAAJAAAA\n" 55 | "AACkAAAKpKQAAAAAAAAAAMAAAAAAAAAAAP7+/v7+9/cAAAAAAAAAAAAAAAAAAAAAAKQAAACkAKSk\n" 56 | "AAAAAAAAwAAAAAAAAAAAB/7+/v7+9/cAAAAAAAAAAACkAAAAAAAAAAAAAACkpACkAAAAAADAAAAA\n" 57 | "AAAAAAAH/v7+/v7+9/cAAAAAAAAAAAD3pAAAAAAAAAAAAAAApKQKAAAAAMDAAAAAAAAAAAf+/v7+\n" 58 | "/v7+9/cAAAAAAAAAAAD39wcAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAB/7+/v7+/v7+9/cAAAAA\n" 59 | "AAAAAAD39/4HAAAAAAAAAAAAAAAAAADAAAAACqQAAAAH/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+\n" 60 | "BwAAAAAAAAAAAAAAAMDAAAAKpAoAAAf+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/gcAAAAAAAAA\n" 61 | "AAAAAAAAAAqkpAAAB/7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v4HAAAAAAAAAAAAAAAAAKQK\n" 62 | "AAAH/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+BwAAAAAAwADAAAAAAAAAAAf+/v7+/v7+\n" 63 | "/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/gcAAAAAAMDAwAAAAAAAB/7+/v7+/v7+/v7+/v7+9/cA\n" 64 | "AAAAAAAAAAD39/7+/v7+/v4HAAAAwAAAwAAAAAAH/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD3\n" 65 | "9/7+/v7+/v7+BwAAAAAAwAAAAAf+/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/v7+\n" 66 | "/gcAAAAAAAAAB/7+/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/v7+/v4HAAAAAAAH\n" 67 | "/v7+/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/v7+/v7+BwAAAAf+/v7+/v7+/v7+\n" 68 | "/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/v7+/v7+/gcAB/7+/v7+/v7+/v7+/v7+/v7+/v7+\n" 69 | "9/cAAAAAAAAAAAD39/7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAA\n" 70 | "AAD39/f+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v739/cAAAAAAAAAAAD3+/v79/f3\n" 71 | "9/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/cAAAAAAAAAAAD30NDQ9/f39/f39/f39/f3\n" 72 | "9/f39/f39/f39/f39/f396SkpKSk9/cAAAAAAAAAAAD3+ff59/f39/f39/f39/f39/f39/f39/f3\n" 73 | "9/f39/f39/f39/f39/cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 74 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 75 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////wAA////\n" 76 | "////AAD//h////8AAP/8D////wAA//gP////AAD/8Af///8AAP/gAAAP/wAA/8AAAD//AAD/gAAB\n" 77 | "//8AAP8AAAH//wAA/gAAAAADAAD8AAAAAAMAAPgAAAAAAwAA8AAAAAADAADgAAAAAAMAAMAAAAAA\n" 78 | "AwAAgAAAAAADAACAAAAAAAMAAIAAAAAAAwAAgAAAAAADAADAAAAAAAMAAPAAAAAAAwAA+AAAAAAD\n" 79 | "AAD8AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+AAAAAAMA\n" 80 | "AP4AAAAAAwAA/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA\n" 81 | "/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+\n" 82 | "AAAAAAMAAP///////wAA////////AAD///////8AAA==\n" 83 | ).decode("base64_codec"), 84 | "mc5b.ico": ( 85 | "AAABAAEAICAQAAAAAADoAgAAFgAAACgAAAAgAAAAQAAAAAEABAAAAAAAgAIAAAAAAAAAAAAAAAAA\n" 86 | "AAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA\n" 87 | "/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 88 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAABwAAAAAAAAAAAAAAAAAA\n" 89 | "AHAAYAAAAAAAAAAAAAAAAAcABmYAAAAAAAAAAAAAAKBwAGZmYABwAAAAAAAAAACqAAZmZmYABwAA\n" 90 | "AAAAAAAAqqBmZmZgAABwAAAAAAAAB6qqZmZmAAAABwAACqqqqqqiqqZmYAAAAABwAAqqqqqqoiqq\n" 91 | "ZgBwcAAABwAKoiIiIiIiqqAHBwAAAABwCqIiIiIiIiqqcHBwAAAABwqiIiIiIiIqqgcHBwAAAAAK\n" 92 | "oiIiIiIiqqBwcHAABAAACqqqqqqiKqoHB3cAAEAAAAqqqqqqoqqgAAB3AAwAAAcAAAAAAKqqAAAA\n" 93 | "AABAAABwAAAAAACqpwAAAAAMAHAHAAAAAAAAqgBwAAAAAAdwcAAAAAAAAKAABwAAxAAABwAAAAAA\n" 94 | "AAAAAABwAEzAAHAAAAAAAAAAAAAABwAAwAcAAAAAAAAAAAAAAABwAABwAAAAAAAAAAAAAAAABwAH\n" 95 | "AAAAAAAAAAAAAAAAAABwcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 96 | "AAAAAAAAAAAAAAD///////////////////////8H///+B////AP///gB///QAH//wAA//8AAH/+A\n" 97 | "AA+AAAAHgAAAA4AAAAGAAAAAgAAAAIAAAACAAAAAgAAAAP/AAAH/wAAD/8wAB//eAA///wAf//+A\n" 98 | "P///wH///+D////x/////////////////w==\n" 99 | ).decode("base64_codec"), 100 | "mc2.ico": ( 101 | "AAABAAEAMDAAAQAAAACoDgAAFgAAACgAAAAwAAAAYAAAAAEACAAAAAAAgAoAAAAAAAAAAAAAAAAA\n" 102 | "AAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAMDcwADwyqYAACBAAAAgYAAAIIAA\n" 103 | "ACCgAAAgwAAAIOAAAEAAAABAIAAAQEAAAEBgAABAgAAAQKAAAEDAAABA4AAAYAAAAGAgAABgQAAA\n" 104 | "YGAAAGCAAABgoAAAYMAAAGDgAACAAAAAgCAAAIBAAACAYAAAgIAAAICgAACAwAAAgOAAAKAAAACg\n" 105 | "IAAAoEAAAKBgAACggAAAoKAAAKDAAACg4AAAwAAAAMAgAADAQAAAwGAAAMCAAADAoAAAwMAAAMDg\n" 106 | "AADgAAAA4CAAAOBAAADgYAAA4IAAAOCgAADgwAAA4OAAQAAAAEAAIABAAEAAQABgAEAAgABAAKAA\n" 107 | "QADAAEAA4ABAIAAAQCAgAEAgQABAIGAAQCCAAEAgoABAIMAAQCDgAEBAAABAQCAAQEBAAEBAYABA\n" 108 | "QIAAQECgAEBAwABAQOAAQGAAAEBgIABAYEAAQGBgAEBggABAYKAAQGDAAEBg4ABAgAAAQIAgAECA\n" 109 | "QABAgGAAQICAAECAoABAgMAAQIDgAECgAABAoCAAQKBAAECgYABAoIAAQKCgAECgwABAoOAAQMAA\n" 110 | "AEDAIABAwEAAQMBgAEDAgABAwKAAQMDAAEDA4ABA4AAAQOAgAEDgQABA4GAAQOCAAEDgoABA4MAA\n" 111 | "QODgAIAAAACAACAAgABAAIAAYACAAIAAgACgAIAAwACAAOAAgCAAAIAgIACAIEAAgCBgAIAggACA\n" 112 | "IKAAgCDAAIAg4ACAQAAAgEAgAIBAQACAQGAAgECAAIBAoACAQMAAgEDgAIBgAACAYCAAgGBAAIBg\n" 113 | "YACAYIAAgGCgAIBgwACAYOAAgIAAAICAIACAgEAAgIBgAICAgACAgKAAgIDAAICA4ACAoAAAgKAg\n" 114 | "AICgQACAoGAAgKCAAICgoACAoMAAgKDgAIDAAACAwCAAgMBAAIDAYACAwIAAgMCgAIDAwACAwOAA\n" 115 | "gOAAAIDgIACA4EAAgOBgAIDggACA4KAAgODAAIDg4ADAAAAAwAAgAMAAQADAAGAAwACAAMAAoADA\n" 116 | "AMAAwADgAMAgAADAICAAwCBAAMAgYADAIIAAwCCgAMAgwADAIOAAwEAAAMBAIADAQEAAwEBgAMBA\n" 117 | "gADAQKAAwEDAAMBA4ADAYAAAwGAgAMBgQADAYGAAwGCAAMBgoADAYMAAwGDgAMCAAADAgCAAwIBA\n" 118 | "AMCAYADAgIAAwICgAMCAwADAgOAAwKAAAMCgIADAoEAAwKBgAMCggADAoKAAwKDAAMCg4ADAwAAA\n" 119 | "wMAgAMDAQADAwGAAwMCAAMDAoADw+/8ApKCgAICAgAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD/\n" 120 | "//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 121 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 122 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 123 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKQAAKQAAAAAAAAAAAAAAAAA\n" 124 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkpKSkpKSkpAAAAACkpKSkpKSkpKSkpKSkpKSkpKSkpKSk\n" 125 | "pKQAAAAAAAAAAAAAAKSkpKSkpKQAAAAAAAAApKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkAAAAAAAA\n" 126 | "AAAAAKSkpKSkpAcAAAAAAAAAB6SkpKSkpKSkpKSkpKSkpKSkpKSkpKSkAAAAAAAAAAAAAKSkpKSk\n" 127 | "BwAAAAAACQAAAACkpKSkpKSkpKSkpKSkpKSkpKQAAACkAAAAAAAAAAAAAKSkpKQHAAAAAAAJCQkA\n" 128 | "AAAHpKSkpKSkpKSkpKSkpKSkpKQAAACkAAAAAAAAAAAAAKSkpAcAAAAAAAkJCQkJAAAAAAekpKSk\n" 129 | "pKSkpKSkpKSkpKQAAACkAAAAAAAAAAAAAKSkBwAAAAAACQkJCQkJCQAAAAAHpKSkpKSkpKSkpKSk\n" 130 | "pKSkpKSkAAAAAAAAAAAAAKQAAAAAAAAJCQkJCQkJCQkAAAAAB6SkpKSkpKSkpKSkpKSkpKSkAAAA\n" 131 | "AAAAAAAAAKQAAAAAAAkJCQkJCQkJCQkJAAAAAAekpKSkpKSkpKSkpKSkpKSkAAAAAAAAAAAApAAA\n" 132 | "AAAACQkJCQkJCQkJCQkAAAAAAAAHpKSkpKSkpKSkpKSkpKSkAAAAAAAAAACkAAAAAAAJCQkJCQkJ\n" 133 | "CQkJCQAAAAAAAAAAB6SkpKSkpKSkpKSkpKSkAAAAAAAAAKQAAAAAAAkJCQkJCQkJCQkJAAAAAAAA\n" 134 | "AAAAAAekpKSkpKSkpKSkpKSkAAAAAAAApAAAAAAACQkJCQkJCQkJCQkAAAAAAAAAAAAAAAAHpKSk\n" 135 | "pKSkpKSkpKSkAAAAAACkAAAAAAAJCQkJCQkJCQkJCQAApAAApAAAAAAAAAAAB6SkpKSkpKSkpKSk\n" 136 | "AAAAAAAAAAAAAAkJCQkJCQkJCQkJAACkAACkAAAAAAAAAAAAAAekpKSkpKSkpKSkAAAAAAAAAAAA\n" 137 | "CQkJCQkJCQkJCQkAAKQAAKQAAAAAAAAAAAAAAAAHpKSkpKSkpKSkAAAAAACkAAAAAAkJCQkJCQkJ\n" 138 | "CQAApAAApAAAAKQAAAAAAAAAAAAAB6SkpKSkpKSkAAAAAAAApAAAAAAJCQkJCQkJAACkAACkAAAA\n" 139 | "pACkAAAAAAAAAAAAAAekpKSkpKSkAAAAAAAAAACkAAAACQkJCQkAAKQAAKQAAACkAKQAAAAAAAAA\n" 140 | "AAAAAAAHpKSkpKSkAAAAAAAAAAAAAAAAAAkJCQAAAAAApAAAAAAApAAAAAAAAADAAAAAAAAAB6Sk\n" 141 | "pKSkAAAAAAAAAAAAAAAAAAAJAAAAAACkAAAKpKQAAAAAAAAAAMAAAAAAAAAAAKSkpKSkAAAAAAAA\n" 142 | "AAAAAAAAAAAAAAAAAKQAAACkAKSkAAAAAAAAwAAAAAAAAAAAB6SkpKSkAAAAAAAAAAAAAKQAAAAA\n" 143 | "AAAAAAAAAACkpACkAAAAAADAAAAAAAAAAAAHpKSkpKSkAAAAAAAAAAAAAKSkBwAAAAAAAAAAAAAA\n" 144 | "pKQKAAAAAMDAAAAAAAAAAAekpKSkpKSkAAAAAAAAAAAAAKSkpAcAAAAAAAAAAAAAAAAAAAAAwAAA\n" 145 | "AAAAAAAAB6SkpKSkpKSkAAAAAAAAAAAAAKSkpKQHAAAAAAAAAAAAAAAAAADAAAAACqQAAAAHpKSk\n" 146 | "pKSkpKSkAAAAAAAAAAAAAKSkpKSkBwAAAAAAAAAAAAAAAMDAAAAKpAoAAAekpKSkpKSkpKSkAAAA\n" 147 | "AAAAAAAAAKSkpKSkpAcAAAAAAAAAAAAAAAAAAAqkpAAAB6SkpKSkpKSkpKSkAAAAAAAAAAAAAKSk\n" 148 | "pKSkpKQHAAAAAAAAAAAAAAAAAKQKAAD3pKSkpKSkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9wAA\n" 149 | "AAAAwADAAAAAAAAAAPf39/f396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/cAAAAAAMDAwAAA\n" 150 | "AAAAB6SkpPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f3AAAAwAAAwAAAAACkpKSkpPf3\n" 151 | "96SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f39wAAAAAAwAAAAPekpKSkpPf396SkpKSkpKSk\n" 152 | "AAAAAAAAAAAAAKSkpKSkpKSk9/f39/cAAAAAAAAA9/ekpKSkpPf396SkpKSkpKSkAAAAAAAAAAAA\n" 153 | "AKSkpKSkpKSk9/f39/f3AAAAAAD39/ekpKSkpPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk\n" 154 | "9/f39/f39wAAAPf39/ekpKSkpPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f39/f39/cA\n" 155 | "9/f39/ekpKSkpPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f39/f39/f39/f39/ekpKSk\n" 156 | "pPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f39/f39/f39/f39/ekpKSkpPf396SkpKSk\n" 157 | "pKQAAAAAAAAAAAAAAKSkpKSkpKSk9/f39/f39/f39/f39/ekpKSkpPf396SkpKSkpAAA9gAAAAAA\n" 158 | "AAAAAACkpKSkpKSk9/f39/f39/f39/f39/f39/f39/f396SkpKSkAAAAAAAAAAAAAAAAAAAAAAAA\n" 159 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 160 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////AAD///////8AAP///////wAA////////\n" 161 | "AAD//w////8AAP8AAAAADwAA/gAAAAAHAAD+AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAAcA\n" 162 | "AP4AAAAABwAA/gAAAAAHAAD+AAAAAAcAAPwAAAAABwAA+AAAAAAHAADwAAAAAAcAAOAAAAAABwAA\n" 163 | "wAAAAAAHAADAAAAAAAcAAMAAAAAABwAAwAAAAAAHAADgAAAAAAcAAPgAAAAABwAA/AAAAAAHAAD+\n" 164 | "AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAAcAAP4A\n" 165 | "AAAABwAA/gAAAAAHAAD+AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAAcAAP4AAAAABwAA/gAA\n" 166 | "AAAHAAD+AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAA8AAP4AAAAAGwAA/wAAAAA/AAD/////\n" 167 | "//sAAP///////wAA\n" 168 | ).decode("base64_codec"), 169 | "mc6a.ico": ( 170 | "AAABAAEAICAQAAAAAADoAgAAFgAAACgAAAAgAAAAQAAAAAEABAAAAAAAgAIAAAAAAAAAAAAAAAAA\n" 171 | "AAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA\n" 172 | "/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 173 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAcABg\n" 174 | "AAAAAAAAAAAAAAAABwAGZgAAAAAAAAAAAAAAAHAAZmZgAHAAAACgAAAAAAcABmZmZgAHAAAAqgAA\n" 175 | "AABwAGZmZmAAAHAAAKqgAAAHAAZmZmYAAAAHAACqqgAAcABmZmZgAAqqqqqqoqqgAAAGZmZmAHB6\n" 176 | "qqqqqqIqqgAAZmZmYAcHCqIiIiIiIqqgAAZmZgBwcHqiIiIiIiIqqnAAZmAHBwcKoiIiIiIiKqoA\n" 177 | "AAYAAHBweqIiIiIiIqqgAAAAAAcHdwqqqqqqoiqqAAAAAAAAAHcKqqqqqqKqoAAAAHAAAAAAAEAA\n" 178 | "AHCqqgAAAAAHAAAAAAwAcAcAqqAAAAAAAHAAAAAAB3BwAKoAAAAAAAAHAADEAAAHAACgAAAAAAAA\n" 179 | "AHAATMAAcAAAAAAAAAAAAAAHAADABwAAAAAAAAAAAAAAAHAAAHAAAAAAAAAAAAAAAAAHAAcAAAAA\n" 180 | "AAAAAAAAAAAAAHBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" 181 | "AAAAAAAAAAAAAAD//////////////////////wf///4H///8A///+AH///AAf3/gAD8/wAAfH4AA\n" 182 | "Dw8AAAAHAAAAAwAAAAEAAAAAAAAAAMAAAAHgAAAD8AAAB/AAAQ/4AAMf/AAHP/4AD3//AB///4A/\n" 183 | "///Af///4P////H//////////////////w==\n" 184 | ).decode("base64_codec"), 185 | } 186 | -------------------------------------------------------------------------------- /lzari.py: -------------------------------------------------------------------------------- 1 | # 2 | # lzari.py 3 | # 4 | # By Ross Ridge 5 | # 6 | 7 | """ 8 | Implementation of Haruhiko Okumura's LZARI data compression algorithm 9 | in Python. Largely based on LZARI.C, one key difference is the use of 10 | a two level dicitionary look up during compression rather than 11 | LZARI.C's binary search tree. 12 | """ 13 | 14 | _SCCS_ID = "@(#) mymc lzari.py 1.6 12/10/04 19:07:53\n" 15 | 16 | import sys 17 | import array 18 | import binascii 19 | import string 20 | import time 21 | from bisect import bisect_right 22 | from math import log 23 | 24 | try: 25 | import ctypes 26 | import mymcsup 27 | except ImportError: 28 | mymcsup = None 29 | 30 | hexlify = binascii.hexlify 31 | 32 | __ALL__ = ['lzari_codec', 'string_to_bit_array', 'bit_array_to_string'] 33 | 34 | # 35 | # Fundamental constants of the LZARI compression alogorithm. 36 | # 37 | # Changing any of these values will create an incompatible implementation. 38 | # 39 | 40 | HIST_LEN = 4096 41 | MIN_MATCH_LEN = 3 42 | MAX_MATCH_LEN = 60 43 | 44 | ARITH_BITS = 15 45 | QUADRANT1 = 1 << ARITH_BITS 46 | QUADRANT2 = QUADRANT1 * 2 47 | QUADRANT3 = QUADRANT1 * 3 48 | QUADRANT4 = QUADRANT1 * 4 49 | MAX_CUM = QUADRANT1 - 1 50 | MAX_CHAR = (256 + MAX_MATCH_LEN - MIN_MATCH_LEN + 1) 51 | 52 | # 53 | # Other constants specific to this implementation 54 | # 55 | 56 | MAX_SUFFIX_CHAIN = 50 # limit on how many identical suffixes to try to match 57 | 58 | #def debug(value, msg): 59 | # print "@@@ %s %04x" % (msg, value) 60 | debug = lambda value, msg: None 61 | 62 | _tr_16 = string.maketrans("0123456789abcdef", 63 | "\x00\x01\x02\x03" 64 | "\x10\x11\x12\x13" 65 | "\x20\x21\x22\x23" 66 | "\x30\x31\x32\x33") 67 | _tr_4 = string.maketrans("0123", 68 | "\x00\x01" 69 | "\x10\x11") 70 | _tr_2 = string.maketrans("01", "\x00\x01") 71 | 72 | def string_to_bit_array(s): 73 | """Convert a string to an array containing a sequence of bits.""" 74 | s = binascii.hexlify(s).translate(_tr_16) 75 | s = binascii.hexlify(s).translate(_tr_4) 76 | s = binascii.hexlify(s).translate(_tr_2) 77 | a = array.array('B', s) 78 | return a 79 | 80 | _tr_rev_2 = string.maketrans("\x00\x01", "01") 81 | _tr_rev_4 = string.maketrans("\x00\x01" 82 | "\x10\x11", 83 | "0123") 84 | _tr_rev_16 = string.maketrans("\x00\x01\x02\x03" 85 | "\x10\x11\x12\x13" 86 | "\x20\x21\x22\x23" 87 | "\x30\x31\x32\x33", 88 | "0123456789abcdef") 89 | def bit_array_to_string(a): 90 | """Convert an array containing a sequence of bits to a string.""" 91 | remainder = len(a) % 8 92 | if remainder != 0: 93 | a.fromlist([0] * (8 - remainder)) 94 | s = a.tostring() 95 | s = binascii.unhexlify(s.translate(_tr_rev_2)) 96 | s = binascii.unhexlify(s.translate(_tr_rev_4)) 97 | return binascii.unhexlify(s.translate(_tr_rev_16)) 98 | 99 | def _match(src, pos, hpos, mlen, end): 100 | mlen += 1 101 | if not src.startswith(src[hpos : hpos + mlen], pos): 102 | return None 103 | for i in range(mlen, end): 104 | if src[pos + i] != src[hpos + i]: 105 | return i 106 | return end 107 | 108 | def _rehash_table2(src, chars, head, next, next2, hist_invalid): 109 | p = head 110 | table2 = {} 111 | l = [] 112 | while p > hist_invalid: 113 | l.append(p) 114 | p = next[p % HIST_LEN] 115 | l.reverse() 116 | for p in l: 117 | p2 = p + MIN_MATCH_LEN 118 | key2 = src[p2 : p2 + chars] 119 | head2 = table2.get(key2, hist_invalid) 120 | next2[p % HIST_LEN] = head2 121 | table2[key2] = p 122 | return table2 123 | 124 | class lzari_codec(object): 125 | # despite the name this does not implement a codec compatible 126 | # with Python's codec system 127 | 128 | def init(self, decode): 129 | self.high = QUADRANT4 130 | self.low = 0 131 | if decode: 132 | self.code = 0 133 | # reverse the order of sym_cum so bisect_right() can 134 | # be used for faster searching 135 | self.sym_cum = range(0, MAX_CHAR + 1) 136 | else: 137 | self.shifts = 0 138 | self.char_to_symbol = range(1, MAX_CHAR + 1) 139 | self.sym_cum = range(MAX_CHAR, -1, -1) 140 | self.next_table = [None] * HIST_LEN 141 | self.next2_table = [None] * HIST_LEN 142 | self.suffix_table = {} 143 | 144 | self.symbol_to_char = [0] + range(MAX_CHAR) 145 | self.sym_freq = [0] + [1] * MAX_CHAR 146 | self.position_cum = [0] * (HIST_LEN + 1) 147 | a = 0 148 | for i in range(HIST_LEN, 0, -1): 149 | a = a + 10000 / (200 + i) 150 | self.position_cum[i - 1] = a 151 | 152 | def search(self, table, x): 153 | c = 1 154 | s = len(table) - 1 155 | while True: 156 | a = (s + c) / 2 157 | if table[a] <= x: 158 | s = a 159 | else: 160 | c = a + 1 161 | if c >= s: 162 | break 163 | return c 164 | 165 | def update_model_decode(self, symbol): 166 | # A compatible implemention to the one used while compressing. 167 | 168 | sym_freq = self.sym_freq 169 | sym_cum = self.sym_cum 170 | 171 | if self.sym_cum[MAX_CHAR] >= MAX_CUM: 172 | c = 0 173 | for i in range(MAX_CHAR, 0, -1): 174 | self.sym_cum[MAX_CHAR - i] = c 175 | a = (self.sym_freq[i] + 1) / 2 176 | self.sym_freq[i] = a 177 | c += a 178 | self.sym_cum[MAX_CHAR] = c 179 | freq = sym_freq[symbol] 180 | new_symbol = symbol 181 | while self.sym_freq[new_symbol - 1] == freq: 182 | new_symbol -= 1 183 | # new_symbol = sym_freq.index(freq) 184 | if new_symbol != symbol: 185 | symbol_to_char = self.symbol_to_char 186 | swap_char = symbol_to_char[new_symbol] 187 | char = symbol_to_char[symbol] 188 | symbol_to_char[new_symbol] = char 189 | symbol_to_char[symbol] = swap_char 190 | sym_freq[new_symbol] = freq + 1 191 | for i in range(MAX_CHAR - new_symbol + 1, MAX_CHAR + 1): 192 | sym_cum[i] += 1 193 | 194 | def update_model_encode(self, symbol): 195 | sym_freq = self.sym_freq 196 | sym_cum = self.sym_cum 197 | 198 | if sym_cum[0] >= MAX_CUM: 199 | c = 0 200 | for i in range(MAX_CHAR, 0, -1): 201 | sym_cum[i] = c 202 | a = (sym_freq[i] + 1) / 2 203 | sym_freq[i] = a 204 | c += a 205 | sym_cum[0] = c 206 | freq = sym_freq[symbol] 207 | new_symbol = symbol 208 | while sym_freq[new_symbol - 1] == freq: 209 | new_symbol -= 1 210 | if new_symbol != symbol: 211 | debug(new_symbol, "a") 212 | swap_char = self.symbol_to_char[new_symbol] 213 | char = self.symbol_to_char[symbol] 214 | self.symbol_to_char[new_symbol] = char 215 | self.symbol_to_char[symbol] = swap_char 216 | self.char_to_symbol[char] = new_symbol 217 | self.char_to_symbol[swap_char] = symbol 218 | sym_freq[new_symbol] += 1 219 | for i in range(new_symbol): 220 | sym_cum[i] += 1 221 | 222 | def decode_char(self): 223 | high = self.high 224 | low = self.low 225 | code = self.code 226 | sym_cum = self.sym_cum 227 | 228 | _range = high - low 229 | max_cum_freq = sym_cum[MAX_CHAR] 230 | n = ((code - low + 1) * max_cum_freq - 1) / _range 231 | i = bisect_right(sym_cum, n, 1) 232 | high = low + sym_cum[i] * _range / max_cum_freq 233 | low += sym_cum[i - 1] * _range / max_cum_freq 234 | symbol = MAX_CHAR + 1 - i 235 | 236 | while True: 237 | if low < QUADRANT2: 238 | if low < QUADRANT1 or high > QUADRANT3: 239 | if high > QUADRANT2: 240 | break 241 | else: 242 | low -= QUADRANT1 243 | code -= QUADRANT1 244 | high -= QUADRANT1 245 | else: 246 | low -= QUADRANT2 247 | code -= QUADRANT2 248 | high -= QUADRANT2 249 | low *= 2 250 | high *= 2 251 | code = code * 2 + self.in_iter() 252 | 253 | ret = self.symbol_to_char[symbol] 254 | self.high = high 255 | self.low = low 256 | self.code = code 257 | self.update_model_decode(symbol) 258 | return ret 259 | 260 | def decode_position(self): 261 | _range = self.high - self.low 262 | max_cum = self.position_cum[0] 263 | pos = self.search(self.position_cum, 264 | ((self.code - self.low + 1) 265 | * max_cum - 1) / _range) - 1 266 | self.high = (self.low + 267 | self.position_cum[pos] * _range / max_cum) 268 | self.low += self.position_cum[pos + 1] * _range / max_cum 269 | while True: 270 | if self.low < QUADRANT2: 271 | if (self.low < QUADRANT1 272 | or self.high > QUADRANT3): 273 | if self.high > QUADRANT2: 274 | return pos 275 | else: 276 | self.low -= QUADRANT1 277 | self.code -= QUADRANT1 278 | self.high -= QUADRANT1 279 | else: 280 | self.low -= QUADRANT2 281 | self.code -= QUADRANT2 282 | self.high -= QUADRANT2 283 | self.low *= 2 284 | self.high *= 2 285 | self.code = self.in_iter() + self.code * 2 286 | 287 | def add_suffix_1(self, pos, find): 288 | # naive implemention used for testing 289 | 290 | if not find: 291 | return (None, 0) 292 | src = self.src 293 | mlen = min(1000, self.max_match, len(src) - pos) 294 | hist_start = max(pos - HIST_LEN, 0) 295 | while mlen >= MIN_MATCH_LEN: 296 | i = src.rfind(src[pos : pos + mlen], hist_start, pos) 297 | if i != -1: 298 | assert (src[pos : pos + mlen] 299 | == src[i: i + mlen]) 300 | return (i, mlen) 301 | mlen -= 1 302 | return (None, -1) 303 | 304 | def add_suffix_2(self, pos, find): 305 | # a two level dictionary look up that leverages Python's 306 | # built-in dicts to get something that's hopefully faster 307 | # than implementing binary trees in completely in Python. 308 | 309 | src = self.src 310 | suffix_table = self.suffix_table 311 | max_match = min(self.max_match, len(src) - pos) 312 | 313 | mlen = -1 314 | mpos = None 315 | 316 | hist_invalid = pos - HIST_LEN - 1 317 | modpos = pos % HIST_LEN 318 | pos2 = pos + MIN_MATCH_LEN 319 | 320 | key = src[pos : pos2] 321 | a = suffix_table.get(key) 322 | if a != None: 323 | next = self.next_table 324 | next2 = self.next2_table 325 | 326 | [count, head, table2, chars] = a 327 | 328 | pos3 = pos2 + chars 329 | key2 = src[pos2 : pos3] 330 | min_match2 = MIN_MATCH_LEN + chars 331 | if find: 332 | p = table2.get(key2, hist_invalid) 333 | maxmlen = max_match - min_match2 334 | while p > hist_invalid and mlen != maxmlen: 335 | p3 = p + min_match2 336 | if mpos == None and p3 <= pos: 337 | mpos = p 338 | mlen = 0 339 | if p3 >= pos: 340 | p = next2[p % HIST_LEN] 341 | continue 342 | rlen = _match(src, pos3, p3, mlen, 343 | min(maxmlen, pos - p3)) 344 | if rlen != None: 345 | mpos = p 346 | mlen = rlen 347 | p = next2[p % HIST_LEN] 348 | if mpos != None: 349 | mlen += min_match2 350 | elif find: 351 | p = head 352 | maxmlen = min(chars, max_match - MIN_MATCH_LEN) 353 | i = 0 354 | while (p > hist_invalid and i < 50000 355 | and mlen < maxmlen): 356 | assert i < count 357 | i += 1 358 | p2 = p + MIN_MATCH_LEN 359 | l2 = pos - p2 360 | if mpos == None and l2 >= 0: 361 | mpos = p 362 | mlen = 0 363 | if l2 <= 0: 364 | p = next[p % HIST_LEN] 365 | continue 366 | if l2 > maxmlen: 367 | l2 = maxmlen 368 | m = mlen + 1 369 | if src.startswith(src[p2 : p2 + m], 370 | pos2): 371 | mpos = p 372 | for j in range(m, l2): 373 | if (src[pos2 + j] 374 | != src[p2 + j]): 375 | mlen = j 376 | break 377 | else: 378 | mlen = l2 379 | #rlen = _match(src, pos2, p2, mlen, l2) 380 | #if rlen != None: 381 | # mpos = p 382 | # mlen = rlen 383 | p = next[p % HIST_LEN] 384 | 385 | if mpos != None: 386 | mlen += MIN_MATCH_LEN 387 | 388 | count += 1 389 | new_chars = int(log(count, 2)) 390 | # new_chars = 50 391 | new_chars = min(new_chars, max_match - MIN_MATCH_LEN) 392 | if new_chars > chars: 393 | chars = new_chars 394 | table2 = _rehash_table2(src, chars, head, 395 | next, next2, 396 | hist_invalid) 397 | 398 | next[modpos] = head 399 | head = pos 400 | 401 | key2 = src[pos2 : pos2 + chars] 402 | head2 = table2.get(key2, hist_invalid) 403 | next2[modpos] = head2 404 | table2[key2] = pos 405 | 406 | a[0] = count 407 | a[1] = head 408 | a[2] = table2 409 | a[3] = chars 410 | else: 411 | self.next_table[modpos] = hist_invalid 412 | self.next2_table[modpos] = hist_invalid 413 | key2 = "" 414 | # key2 = src[pos2 : pos2 + 1] 415 | suffix_table[key] = [1, pos, {key2: pos}, len(key2)] 416 | 417 | p = pos - HIST_LEN 418 | if p >= 0: 419 | p2 = p + MIN_MATCH_LEN 420 | key = src[p : p2] 421 | a = suffix_table[key] 422 | (count, head, table2, chars) = a 423 | count -= 1 424 | if count == 0: 425 | assert head == p 426 | del suffix_table[key] 427 | else: 428 | key2 = src[p2 : p2 + chars] 429 | if table2[key2] == p: 430 | del table2[key2] 431 | a[0] = count 432 | assert (mpos == None 433 | or src[pos : pos + mlen] == src[mpos : mpos + mlen]) 434 | return (mpos, mlen) 435 | 436 | def _add_suffix(self, pos, find): 437 | r = self.add_suffix_2(pos, find) 438 | start_pos = self.start_pos 439 | if find and r[0] != None: 440 | print ("%4d %02x %4d %2d" 441 | % (pos - start_pos, ord(self.src[pos]), 442 | r[0] - start_pos, r[1])) 443 | else: 444 | print ("%4d %02x" 445 | % (pos - start_pos, ord(self.src[pos]))) 446 | return r 447 | 448 | add_suffix = add_suffix_2 449 | 450 | def output_bit(self, bit): 451 | self.append_bit(bit) 452 | bit ^= 1 453 | for i in range(self.shifts): 454 | self.append_bit(bit) 455 | self.shifts = 0 456 | 457 | def encode_char(self, char): 458 | low = self.low 459 | high = self.high 460 | sym_cum = self.sym_cum 461 | 462 | symbol = self.char_to_symbol[char] 463 | range = high - low 464 | 465 | high = low + range * sym_cum[symbol - 1] / sym_cum[0] 466 | low += range * sym_cum[symbol] / sym_cum[0] 467 | debug(high, "high"); 468 | debug(low, "low"); 469 | while True: 470 | if high <= QUADRANT2: 471 | self.output_bit(0) 472 | elif low >= QUADRANT2: 473 | self.output_bit(1) 474 | low -= QUADRANT2 475 | high -= QUADRANT2 476 | elif low >= QUADRANT1 and high <= QUADRANT3: 477 | self.shifts += 1 478 | low -= QUADRANT1 479 | high -= QUADRANT1 480 | else: 481 | break 482 | low *= 2 483 | high *= 2 484 | self.low = low 485 | self.high = high 486 | self.update_model_encode(symbol) 487 | 488 | def encode_position(self, position): 489 | position_cum = self.position_cum 490 | low = self.low 491 | high = self.high 492 | 493 | range = high - low 494 | high = low + range * position_cum[position] / position_cum[0] 495 | low += range * position_cum[position + 1] / position_cum[0] 496 | 497 | debug(high, "high"); 498 | debug(low, "low"); 499 | while True: 500 | if high <= QUADRANT2: 501 | self.output_bit(0) 502 | elif low >= QUADRANT2: 503 | self.output_bit(1) 504 | low -= QUADRANT2 505 | high -= QUADRANT2 506 | elif low >= QUADRANT1 and high <= QUADRANT3: 507 | self.shifts += 1 508 | low -= QUADRANT1 509 | high -= QUADRANT1 510 | else: 511 | break 512 | low *= 2 513 | high *= 2 514 | 515 | self.low = low 516 | self.high = high 517 | 518 | def encode(self, src, progress = None): 519 | """Compress a string.""" 520 | 521 | length = len(src) 522 | if length == 0: 523 | return "" 524 | 525 | out_array = array.array('B') 526 | self.out_array = out_array 527 | self.append_bit = out_array.append 528 | 529 | self.init(False) 530 | 531 | max_match = min(MAX_MATCH_LEN, length) 532 | self.max_match = max_match 533 | self.src = src = "\x20" * max_match + src 534 | 535 | in_length = len(src) 536 | 537 | self.start_pos = max_match 538 | 539 | for in_pos in range(max_match): 540 | self.add_suffix(in_pos, False) 541 | in_pos += 1 542 | last_percent = -1 543 | while in_pos < in_length: 544 | if progress: 545 | percent = (in_pos - max_match) * 100 / length 546 | if percent != last_percent: 547 | sys.stderr.write("%s%3d%%\r" 548 | % (progress, percent)) 549 | last_percent = percent 550 | debug(ord(src[in_pos]), "src") 551 | (match_pos, match_len) = self.add_suffix(in_pos, True) 552 | if match_len < MIN_MATCH_LEN: 553 | self.encode_char(ord(src[in_pos])) 554 | else: 555 | debug(in_pos - match_pos - 1, "match_pos") 556 | debug(match_len, "match_len") 557 | self.encode_char(256 - MIN_MATCH_LEN 558 | + match_len) 559 | self.encode_position(in_pos - match_pos - 1) 560 | for i in range(match_len - 1): 561 | in_pos += 1 562 | self.add_suffix(in_pos, False) 563 | in_pos += 1 564 | 565 | self.shifts += 1 566 | if self.low < QUADRANT1: 567 | self.output_bit(0) 568 | else: 569 | self.output_bit(1) 570 | 571 | #for k, v in sorted(self.suffix_table.items()): 572 | # count, head, table2, chars = v 573 | # print hexlify(k), count, head, len(table2), chars 574 | 575 | if progress: 576 | sys.stderr.write("%s100%%\n" % progress) 577 | 578 | return bit_array_to_string(out_array) 579 | 580 | def decode(self, src, out_length, progress = None): 581 | """Decompress a string.""" 582 | 583 | a = string_to_bit_array(src) 584 | a.fromlist([0] * 32) # add some extra bits 585 | self.in_iter = iter(a).next 586 | 587 | out = array.array('B', "\0") * out_length 588 | outpos = 0 589 | 590 | self.init(True) 591 | 592 | self.code = 0 593 | for i in range(ARITH_BITS + 2): 594 | self.code += self.code + self.in_iter() 595 | 596 | hist_pos = HIST_LEN - MAX_MATCH_LEN 597 | history = [0x20] * hist_pos + [0] * MAX_MATCH_LEN 598 | 599 | decode_char = self.decode_char 600 | last_percent = -1 601 | last_time = time.time() 602 | while outpos < out_length: 603 | if progress: 604 | percent = outpos * 100 / out_length 605 | if percent != last_percent: 606 | now = time.time() 607 | if now - last_time >= 1: 608 | sys.stderr.write("%s%3d%%\r" 609 | % (progress, percent)) 610 | last_percent = percent 611 | last_time = now 612 | char = decode_char() 613 | if char >= 0x100: 614 | pos = self.decode_position() 615 | length = char - 0x100 + MIN_MATCH_LEN 616 | base = (hist_pos - pos - 1) % HIST_LEN 617 | for off in range(length): 618 | a = history[(base + off) % HIST_LEN] 619 | out[outpos] = a 620 | outpos += 1 621 | history[hist_pos] = a 622 | hist_pos = (hist_pos + 1) % HIST_LEN 623 | else: 624 | out[outpos] = char 625 | outpos += 1 626 | history[hist_pos] = char 627 | hist_pos = (hist_pos + 1) % HIST_LEN 628 | 629 | self.in_iter = None 630 | if progress: 631 | sys.stderr.write("%s100%%\n" % progress) 632 | return out.tostring() 633 | 634 | if mymcsup == None: 635 | def decode(src, out_length, progress = None): 636 | return lzari_codec().decode(src, out_length, progress) 637 | 638 | def encode(src, progress = None): 639 | return lzari_codec().encode(src, progress) 640 | else: 641 | mylzari_decode = mymcsup.mylzari_decode 642 | mylzari_encode = mymcsup.mylzari_encode 643 | mylzari_free_encoded = mymcsup.mylzari_free_encoded 644 | 645 | def decode(src, out_length, progress = None): 646 | out = ctypes.create_string_buffer(out_length) 647 | if (mylzari_decode(src, len(src), out, out_length, progress) 648 | == -1): 649 | raise ValueError, "compressed input is corrupt" 650 | return ctypes.string_at(out, out_length) 651 | 652 | def encode(src, progress = None): 653 | (r, compressed, comp_len) = mylzari_encode(src, len(src), 654 | progress) 655 | # print r, compressed.value, comp_len 656 | if r == -1: 657 | raise MemoryError, "out of memory during compression" 658 | if compressed.value == None: 659 | return "" 660 | ret = ctypes.string_at(compressed.value, comp_len.value) 661 | mylzari_free_encoded(compressed) 662 | return ret; 663 | 664 | def main2(args): 665 | import struct 666 | import os 667 | 668 | src = file(args[2], "rb").read() 669 | lzari = lzari_codec() 670 | out = file(args[3], "wb") 671 | start = os.times() 672 | if args[1] == "c": 673 | dest = lzari.encode(src) 674 | now = os.times() 675 | out.write(struct.pack("L", len(src))) 676 | else: 677 | dest = lzari.decode(src[4:], 678 | struct.unpack("L", src[:4])[0]) 679 | now = os.times() 680 | out.write(dest) 681 | out.close() 682 | print "time:", now[0] - start[0], now[1] - start[1], now[4] - start[4] 683 | 684 | 685 | def _get_hotshot_lineinfo(filename): 686 | import hotshot.log 687 | log = hotshot.log.LogReader(filename) 688 | timings = {} 689 | for what, loc, tdelta in log: 690 | if what == hotshot.log.LINE: 691 | a = timings.get(loc) 692 | if a == None: 693 | timings[loc] = [1, tdelta] 694 | else: 695 | a[0] += 1 696 | a[1] += tdelta 697 | return timings.items() 698 | 699 | def _dump_hotshot_lineinfo(log): 700 | a = sorted(_get_hotshot_lineinfo(log)) 701 | total_count = sum((time[0] 702 | for (loc, time) in a)) 703 | total_time = sum((time[1] 704 | for (loc, time) in a)) 705 | for (loc, [count, time]) in a: 706 | print ("%8d %6.3f%% %8d %6.3f%%" 707 | % (time, time * 100.0 / total_time, 708 | count, count * 100.0 / total_count)), 709 | print "%s:%d(%s)" % loc 710 | 711 | def _dump_hotshot_lineinfo2(log): 712 | cur = None 713 | a = sorted(_get_hotshot_lineinfo(log)) 714 | total_count = sum((time[0] 715 | for (loc, time) in a)) 716 | total_time = sum((time[1] 717 | for (loc, time) in a)) 718 | for ((filename, lineno, fn), [count, time]) in a: 719 | if cur != filename: 720 | if cur != None and f != None: 721 | for line in f: 722 | print line[:-1] 723 | f.close() 724 | try: 725 | f = file(filename, "r") 726 | except OSError: 727 | f = None 728 | cur = filename 729 | l = 0 730 | print "#", filename 731 | if f != None: 732 | while l < lineno: 733 | print f.readline()[:-1] 734 | l += 1 735 | print ("# %8d %6.3f%% %8d %6.3f%%" 736 | % (time, time * 100.0 / total_time, 737 | count, count * 100.0 / total_count)) 738 | if cur != None and f != None: 739 | for line in f: 740 | print line[:-1] 741 | f.close() 742 | 743 | def main(args): 744 | import os 745 | 746 | if args[1] == "pc": 747 | import profile 748 | pr = profile.Profile() 749 | for i in range(5): 750 | print pr.calibrate(100000) 751 | return 752 | elif args[1] == "p": 753 | import profile 754 | ret = 0 755 | # profile.Profile.bias = 5.26e-6 756 | profile.runctx("ret = main2(args[1:])", 757 | globals(), locals()) 758 | return ret 759 | elif args[1].startswith("h"): 760 | import hotshot, hotshot.stats 761 | import warnings 762 | 763 | warnings.filterwarnings("ignore") 764 | tmp = os.tempnam() 765 | try: 766 | l = args[1].startswith("hl") 767 | p = hotshot.Profile(tmp, l) 768 | ret = p.runcall(main2, args[1:]) 769 | p.close() 770 | p = None 771 | if l: 772 | if args[1] == "hl2": 773 | _dump_hotshot_lineinfo2(tmp) 774 | else: 775 | _dump_hotshot_lineinfo(tmp) 776 | else: 777 | hotshot.stats.load(tmp).print_stats() 778 | finally: 779 | try: 780 | os.remove(tmp) 781 | except OSError: 782 | pass 783 | return ret 784 | 785 | return main2(args) 786 | 787 | if __name__ == '__main__': 788 | sys.exit(main(sys.argv)) 789 | 790 | -------------------------------------------------------------------------------- /mymc.py: -------------------------------------------------------------------------------- 1 | # 2 | # mymc.py 3 | # 4 | # By Ross Ridge 5 | # Public Domain 6 | # 7 | 8 | """A utility for manipulating PS2 memory card images.""" 9 | 10 | _SCCS_ID = "@(#) mymc mymc.py 1.13 22/01/15 01:04:45\n"[:-1] 11 | 12 | import sys 13 | import os 14 | import time 15 | import optparse 16 | import textwrap 17 | import binascii 18 | import string 19 | from errno import EEXIST, EIO 20 | 21 | #import gc 22 | #gc.set_debug(gc.DEBUG_LEAK) 23 | 24 | import ps2mc 25 | import ps2save 26 | from ps2mc_dir import * 27 | from round import * 28 | import verbuild 29 | 30 | class subopt_error(Exception): 31 | pass 32 | 33 | io_error = ps2mc.io_error 34 | 35 | if os.name == "nt": 36 | import codecs 37 | 38 | class file_wrap(object): 39 | """ wrap a file-like object with a new encoding attribute. """ 40 | 41 | def __init__(self, f, encoding): 42 | object.__setattr__(self, "_f", f) 43 | object.__setattr__(self, "encoding", encoding) 44 | 45 | def __getattribute__(self, name): 46 | if name == "encoding": 47 | return object.__getattribute__(self, name) 48 | return getattr(object.__getattribute__(self, "_f"), 49 | name) 50 | 51 | def __setattr__(self, name, value): 52 | if name == "encoding": 53 | raise TypeError, "readonly attribute" 54 | return setattr(object.__getattribute__(self, "_f"), 55 | name, value) 56 | 57 | for name in ["stdin", "stdout", "stderr"]: 58 | f = getattr(sys, name) 59 | cur = getattr(f, "encoding", None) 60 | if cur == "ascii" or cur == None: 61 | f = file_wrap(f, "mbcs") 62 | else: 63 | try: 64 | codecs.lookup(cur) 65 | except LookupError: 66 | f = file_wrap(f, "mbcs") 67 | setattr(sys, name, f) 68 | 69 | 70 | if os.name in ["nt", "os2", "ce"]: 71 | from glob import glob 72 | else: 73 | # assume globing is done by the shell 74 | glob = lambda pattern: [pattern] 75 | 76 | 77 | def glob_args(args, globfn): 78 | ret = [] 79 | for arg in args: 80 | match = globfn(arg) 81 | if len(match) == 0: 82 | ret.append(arg) 83 | else: 84 | ret += match 85 | return ret 86 | 87 | def _copy(fout, fin): 88 | """copy the contents of one file to another""" 89 | 90 | while True: 91 | s = fin.read(1024) 92 | if s == "": 93 | break 94 | fout.write(s) 95 | 96 | 97 | def do_ls(cmd, mc, opts, args, opterr): 98 | mode_bits = "rwxpfdD81C+KPH4" 99 | 100 | if len(args) == 0: 101 | args = ["/"] 102 | 103 | out = sys.stdout 104 | args = glob_args(args, mc.glob) 105 | for dirname in args: 106 | dir = mc.dir_open(dirname) 107 | try: 108 | if len(args) > 1: 109 | sys.stdout.write("\n" + dirname + ":\n") 110 | for ent in dir: 111 | mode = ent[0] 112 | if (mode & DF_EXISTS) == 0: 113 | continue 114 | for bit in range(0, 15): 115 | if mode & (1 << bit): 116 | out.write(mode_bits[bit]) 117 | else: 118 | out.write("-") 119 | if opts.creation_time: 120 | tod = ent[3] 121 | else: 122 | tod = ent[6] 123 | tm = time.localtime(tod_to_time(tod)) 124 | out.write(" %7d %04d-%02d-%02d" 125 | " %02d:%02d:%02d %s\n" 126 | % (ent[2], 127 | tm.tm_year, tm.tm_mon, tm.tm_mday, 128 | tm.tm_hour, tm.tm_min, tm.tm_sec, 129 | ent[8])) 130 | finally: 131 | dir.close() 132 | 133 | 134 | def do_add(cmd, mc, opts, args, opterr): 135 | if len(args) < 1: 136 | opterr("Filename required.") 137 | if opts.directory != None: 138 | mc.chdir(opts.directory) 139 | for src in glob_args(args, glob): 140 | f = open(src, "rb") 141 | dest = os.path.basename(src) 142 | out = mc.open(dest, "wb") 143 | _copy(out, f) 144 | out.close() 145 | f.close() 146 | 147 | def do_extract(cmd, mc, opts, args, opterr): 148 | if len(args) < 1: 149 | opterr("Filename required.") 150 | 151 | if opts.directory != None: 152 | mc.chdir(opts.directory) 153 | 154 | close_out = False 155 | out = None 156 | if opts.output != None: 157 | if opts.use_stdout: 158 | opterr("The -o and -p options are mutually exclusive.") 159 | dont_close_out = True 160 | out = file(opts.output, "wb") 161 | elif opts.use_stdout: 162 | out = sys.stdout 163 | 164 | try: 165 | for filename in glob_args(args, mc.glob): 166 | f = mc.open(filename, "rb") 167 | try: 168 | if out != None: 169 | _copy(out, f) 170 | continue 171 | a = filename.split("/") 172 | o = file(a[-1], "wb") 173 | try: 174 | _copy(o, f) 175 | finally: 176 | o.close() 177 | finally: 178 | f.close() 179 | finally: 180 | if close_out: 181 | out.close() 182 | 183 | def do_mkdir(cmd, mc, opts, args, opterr): 184 | if len(args) < 1: 185 | opterr("Directory required.") 186 | 187 | for filename in args: 188 | mc.mkdir(filename) 189 | 190 | def do_remove(cmd, mc, opts, args, opterr): 191 | if len(args) < 1: 192 | opterr("Filename required.") 193 | 194 | for filename in args: 195 | mc.remove(filename) 196 | 197 | def do_import(cmd, mc, opts, args, opterr): 198 | if len(args) < 1: 199 | opterr("Filename required.") 200 | 201 | args = glob_args(args, glob) 202 | if opts.directory != None and len(args) > 1: 203 | opterr("The -d option can only be used with a" 204 | "single savefile.") 205 | 206 | for filename in args: 207 | sf = ps2save.ps2_save_file() 208 | f = file(filename, "rb") 209 | try: 210 | ftype = ps2save.detect_file_type(f) 211 | f.seek(0) 212 | if ftype == "max": 213 | sf.load_max_drive(f) 214 | elif ftype == "psu": 215 | sf.load_ems(f) 216 | elif ftype == "cbs": 217 | sf.load_codebreaker(f) 218 | elif ftype == "sps": 219 | sf.load_sharkport(f) 220 | elif ftype == "npo": 221 | raise io_error, (EIO, "nPort saves" 222 | " are not supported.", 223 | filename) 224 | else: 225 | raise io_error, (EIO, "Save file format not" 226 | " recognized", filename) 227 | finally: 228 | f.close() 229 | dirname = opts.directory 230 | if dirname == None: 231 | dirname = sf.get_directory()[8] 232 | print "Importing", filename, "to", dirname 233 | if not mc.import_save_file(sf, opts.ignore_existing, 234 | opts.directory): 235 | print (filename + ": already in memory card image," 236 | " ignored.") 237 | 238 | #re_num = re.compile("[0-9]+") 239 | 240 | def do_export(cmd, mc, opts, args, opterr): 241 | if len(args) < 1: 242 | opterr("Directory name required") 243 | 244 | if opts.overwrite_existing and opts.ignore_existing: 245 | opterr("The -i and -f options are mutually exclusive.") 246 | 247 | args = glob_args(args, mc.glob) 248 | if opts.output_file != None: 249 | if len(args) > 1: 250 | opterr("Only one directory can be exported" 251 | " when the -o option is used.") 252 | if opts.longnames: 253 | opterr("The -o and -l options are mutually exclusive.") 254 | 255 | if opts.directory != None: 256 | os.chdir(opts.directory) 257 | 258 | for dirname in args: 259 | sf = mc.export_save_file(dirname) 260 | filename = opts.output_file 261 | if opts.longnames: 262 | filename = (ps2save.make_longname(dirname, sf) 263 | + "." + opts.type) 264 | if filename == None: 265 | filename = dirname + "." + opts.type 266 | 267 | if not opts.overwrite_existing: 268 | exists = True 269 | try: 270 | open(filename, "rb").close() 271 | except EnvironmentError: 272 | exists = False 273 | if exists: 274 | if opts.ignore_existing: 275 | continue 276 | raise io_error(EEXIST, "File exists", filename) 277 | 278 | f = file(filename, "wb") 279 | try: 280 | print "Exporing", dirname, "to", filename 281 | 282 | if opts.type == "max": 283 | sf.save_max_drive(f) 284 | else: 285 | sf.save_ems(f) 286 | finally: 287 | f.close() 288 | 289 | def do_delete(cmd, mc, opts, args, opterr): 290 | if len(args) < 1: 291 | opterr("Directory required.") 292 | 293 | for dirname in args: 294 | mc.rmdir(dirname) 295 | 296 | def do_setmode(cmd, mc, opts, args, opterr): 297 | set_mask = 0 298 | clear_mask = ~0 299 | for (opt, bit) in [(opts.read, DF_READ), 300 | (opts.write, DF_WRITE), 301 | (opts.execute, DF_EXECUTE), 302 | (opts.protected, DF_PROTECTED), 303 | (opts.psx, DF_PSX), 304 | (opts.pocketstation, DF_POCKETSTN), 305 | (opts.hidden, DF_HIDDEN)]: 306 | if opt != None: 307 | if opt: 308 | set_mask |= bit 309 | else: 310 | clear_mask ^= bit 311 | 312 | value = opts.hex_value 313 | if set_mask == 0 and clear_mask == ~0: 314 | if value == None: 315 | opterr("At least one option must be given.") 316 | if value.startswith("0x") or value.startswith("0X"): 317 | value = int(value[2:], 16) 318 | else: 319 | value = int(value, 16) 320 | else: 321 | if value != None: 322 | opterr("The -X option can't be combined with" 323 | " other options.") 324 | 325 | for arg in glob_args(args, mc.glob): 326 | ent = mc.get_dirent(arg) 327 | if value == None: 328 | ent[0] = (ent[0] & clear_mask) | set_mask 329 | # print "new %04x" % ent[0] 330 | else: 331 | ent[0] = value 332 | mc.set_dirent(arg, ent) 333 | 334 | def do_rename(cmd, mc, opts, args, opterr): 335 | if len(args) != 2: 336 | opterr("Old and new names required") 337 | mc.rename(args[0], args[1]) 338 | 339 | def _get_ps2_title(mc, enc): 340 | s = mc.get_icon_sys("."); 341 | if s == None: 342 | return None 343 | a = ps2save.unpack_icon_sys(s) 344 | return ps2save.icon_sys_title(a, enc) 345 | 346 | def _get_psx_title(mc, savename, enc): 347 | mode = mc.get_mode(savename) 348 | if mode == None or not mode_is_file(mode): 349 | return None 350 | f = mc.open(savename) 351 | s = f.read(128) 352 | if len(s) != 128: 353 | return None 354 | (magic, icon, blocks, title) = struct.unpack("<2sBB64s28x32x", s) 355 | if magic != "SC": 356 | return None 357 | return [ps2save.shift_jis_conv(zero_terminate(title), enc), ""] 358 | 359 | def do_dir(cmd, mc, opts, args, opterr): 360 | if len(args) != 0: 361 | opterr("Incorrect number of arguments.") 362 | f = None 363 | dir = mc.dir_open("/") 364 | try: 365 | for ent in list(dir)[2:]: 366 | dirmode = ent[0] 367 | if not mode_is_dir(dirmode): 368 | continue 369 | dirname = "/" + ent[8] 370 | mc.chdir(dirname) 371 | length = mc.dir_size("."); 372 | enc = getattr(sys.stdout, "encoding", None) 373 | if dirmode & DF_PSX: 374 | title = _get_psx_title(mc, ent[8], enc) 375 | else: 376 | title = _get_ps2_title(mc, enc) 377 | if title == None: 378 | title = ["Corrupt", ""] 379 | protection = dirmode & (DF_PROTECTED | DF_WRITE) 380 | if protection == 0: 381 | protection = "Delete Protected" 382 | elif protection == DF_WRITE: 383 | protection = "Not Protected" 384 | elif protection == DF_PROTECTED: 385 | protection = "Copy & Delete Protected" 386 | else: 387 | protection = "Copy Protected" 388 | 389 | type = None 390 | if dirmode & DF_PSX: 391 | type = "PlayStation" 392 | if dirmode & DF_POCKETSTN: 393 | type = "PocketStation" 394 | if type != None: 395 | protection = type 396 | 397 | print "%-32s %s" % (ent[8], title[0]) 398 | print ("%4dKB %-25s %s" 399 | % (length / 1024, protection, title[1])) 400 | print 401 | finally: 402 | if f != None: 403 | f.close() 404 | dir.close() 405 | 406 | free = mc.get_free_space() / 1024 407 | if free > 999999: 408 | free = "%d,%03d,%03d" % (free / 1000000, free / 1000 % 1000, 409 | free % 1000) 410 | elif free > 999: 411 | free = "%d,%03d" % (free / 1000, free % 1000) 412 | else: 413 | free = "%d" % free 414 | 415 | print free + " KB Free" 416 | 417 | def do_df(cmd, mc, opts, args, opterr): 418 | if len(args) != 0: 419 | opterr("Incorrect number of arguments.") 420 | print mc.f.name + ":", mc.get_free_space(), "bytes free." 421 | 422 | def do_check(cmd, mc, opts, args, opterr): 423 | if len(args) != 0: 424 | opterr("Incorrect number of arguments.") 425 | if mc.check(): 426 | print "No errors found." 427 | return 0 428 | return 1 429 | 430 | def do_format(cmd, mcname, opts, args, opterr): 431 | if len(args) != 0: 432 | opterr("Incorrect number of arguments.") 433 | pages_per_card = ps2mc.PS2MC_STANDARD_PAGES_PER_CARD 434 | if opts.clusters != None: 435 | pages_per_cluster = (ps2mc.PS2MC_CLUSTER_SIZE 436 | / ps2mc.PS2MC_STANDARD_PAGE_SIZE) 437 | pages_per_card = opts.clusters * pages_per_cluster 438 | params = (not opts.no_ecc, 439 | ps2mc.PS2MC_STANDARD_PAGE_SIZE, 440 | ps2mc.PS2MC_STANDARD_PAGES_PER_ERASE_BLOCK, 441 | pages_per_card) 442 | 443 | if not opts.overwrite_existing: 444 | exists = True 445 | try: 446 | file(mcname, "rb").close() 447 | except EnvironmentError: 448 | exists = False 449 | if exists: 450 | raise io_error, (EEXIST, "file exists", mcname) 451 | 452 | f = file(mcname, "w+b") 453 | try: 454 | ps2mc.ps2mc(f, True, params).close() 455 | finally: 456 | f.close() 457 | 458 | def do_gui(cmd, mcname, opts, args, opterr): 459 | if len(args) != 0: 460 | opterr("Incorrect number of arguments.") 461 | 462 | try: 463 | import gui 464 | except ImportError: 465 | write_error(None, "GUI not available") 466 | return 1 467 | 468 | gui.run(mcname) 469 | return 0 470 | 471 | def do_create_pad(cmd, mc, opts, args, opterr): 472 | length = mc.clusters_per_card 473 | if len(args) > 1: 474 | length = int(args[1]) 475 | pad = "\0" * mc.cluster_size 476 | f = mc.open(args[0], "wb") 477 | try: 478 | for i in xrange(length): 479 | f.write(pad) 480 | finally: 481 | f.close() 482 | 483 | 484 | def do_frob(cmd, mc, opts, args, opterr): 485 | mc.write_superblock() 486 | 487 | _trans = string.maketrans("".join(map(chr, range(32))), " " * 32) 488 | 489 | def _print_bin(base, s): 490 | for off in range(0, len(s), 16): 491 | print "%04X" % (base + off), 492 | a = s[off : off + 16] 493 | for b in a: 494 | print "%02X" % ord(b), 495 | print "", a.translate(_trans) 496 | 497 | def _print_erase_block(mc, n): 498 | ppb = mc.pages_per_erase_block 499 | base = n * ppb 500 | for i in range(ppb): 501 | s = mc.read_page(base + i) 502 | _print_bin(i * mc.page_size, s) 503 | print 504 | 505 | def do_print_good_blocks(cmd, mc, opts, args, opterr): 506 | print "good_block2:" 507 | _print_erase_block(mc, mc.good_block2) 508 | print "good_block1:" 509 | _print_erase_block(mc, mc.good_block1) 510 | 511 | def do_ecc_check(cmd, mc, opts, args, opterr): 512 | for i in range(mc.clusters_per_card * mc.pages_per_cluster): 513 | try: 514 | mc.read_page(i) 515 | except ps2mc.ecc_error: 516 | print "bad: %05x" % i 517 | 518 | opt = optparse.make_option 519 | 520 | # 521 | # Each value in the dictionary is a tuple consisting of: 522 | # - function implementing the command 523 | # - mode to use to open the ps2 save file 524 | # - help description of the command 525 | # - list of options supported by the command 526 | # 527 | cmd_table = { 528 | "ls": (do_ls, "rb", 529 | "[directory ...]", 530 | "List the contents of a directory.", 531 | [opt("-c", "--creation-time", action="store_true", 532 | help = "Display creation times.")]), 533 | "extract": (do_extract, "rb", 534 | "filename ...", 535 | "Extract files from the memory card.", 536 | [opt("-o", "--output", metavar = "FILE", 537 | help = 'Extract file to "FILE".'), 538 | opt("-d", "--directory", 539 | help = 'Extract files from "DIRECTORY".'), 540 | opt("-p", "--use-stdout", action="store_true", 541 | help = "Extract files to standard output.")]), 542 | "add": (do_add, "r+b", 543 | "filename ...", 544 | "Add files to the memory card.", 545 | [opt("-d", "--directory", 546 | help = 'Add files to "directory".')]), 547 | "mkdir": (do_mkdir, "r+b", 548 | "directory ...", 549 | "Make directories.", 550 | []), 551 | "remove": (do_remove, "r+b", 552 | "filename ...", 553 | "Remove files and directories.", 554 | []), 555 | "import": (do_import, "r+b", 556 | "savefile ...", 557 | "Import save files into the memory card.", 558 | [opt("-i", "--ignore-existing", action="store_true", 559 | help = ("Ignore files that already exist" 560 | " on the image.")), 561 | opt("-d", "--directory", metavar="DEST", 562 | help = 'Import to "DEST".')]), 563 | "export": (do_export, "rb", 564 | "directory ...", 565 | "Export save files from the memory card.", 566 | [opt("-f", "--overwrite-existing", action = "store_true", 567 | help = "Overwrite any save files already exported."), 568 | opt("-i", "--ignore-existing", action = "store_true", 569 | help = "Ingore any save files already exported."), 570 | opt("-o", "--output-file", metavar = "filename", 571 | help = 'Use "filename" as the name of the save file.'), 572 | opt("-d", "--directory", metavar = "directory", 573 | help = 'Export save files to "directory".'), 574 | opt("-l", "--longnames", action = "store_true", 575 | help = ("Generate longer, more descriptive," 576 | " filenames.")), 577 | opt("-p", "--ems", action = "store_const", 578 | dest = "type", const = "psu", default = "psu", 579 | help = "Use the EMS .psu save file format. [default]"), 580 | opt("-m", "--max-drive", action = "store_const", 581 | dest = "type", const = "max", 582 | help = "Use the MAX Drive save file format.")]), 583 | "delete": (do_delete, "r+b", 584 | "dirname ...", 585 | "Recursively delete a directory (save file).", 586 | []), 587 | "set": (do_setmode, "r+b", 588 | "filename ...", 589 | "Set mode flags on files and directories", 590 | [opt("-p", "--protected", action="store_true", 591 | help = "Set copy protected flag"), 592 | opt("-P", "--psx", action="store_true", 593 | help = "Set PSX flag"), 594 | opt("-K", "--pocketstation", action="store_true", 595 | help = "Set PocketStation flag"), 596 | opt("-H", "--hidden", action="store_true", 597 | help = "Set hidden flag"), 598 | opt("-r", "--read", action="store_true", 599 | help = "Set read allowed flag"), 600 | opt("-w", "--write", action="store_true", 601 | help = "Set write allowed flag"), 602 | opt("-x", "--execute", action="store_true", 603 | help = "Set executable flag"), 604 | opt("-X", "--hex-value", metavar="mode", 605 | help = 'Set mode to "mode".')]), 606 | "clear": (do_setmode, "r+b", 607 | "filename ...", 608 | "Clear mode flags on files and directories", 609 | [opt("-p", "--protected", action="store_false", 610 | help = "Clear copy protected flag"), 611 | opt("-P", "--psx", action="store_false", 612 | help = "Clear PSX flag"), 613 | opt("-K", "--pocketstation", action="store_false", 614 | help = "Clear PocketStation flag"), 615 | opt("-H", "--hidden", action="store_false", 616 | help = "Clear hidden flag"), 617 | opt("-r", "--read", action="store_false", 618 | help = "Clear read allowed flag"), 619 | opt("-w", "--write", action="store_false", 620 | help = "Clear write allowed flag"), 621 | opt("-x", "--execute", action="store_false", 622 | help = "Clear executable flag"), 623 | opt("-X", dest="hex_value", default=None, 624 | help = optparse.SUPPRESS_HELP)]), 625 | "rename": (do_rename, "r+b", 626 | "oldname newname", 627 | "Rename a file or directory", 628 | []), 629 | "dir": (do_dir, "rb", 630 | None, 631 | "Display save file information.", 632 | []), 633 | "df": (do_df, "rb", 634 | None, 635 | "Display the amount free space.", 636 | []), 637 | "check": (do_check, "rb", 638 | "", 639 | "Check for file system errors.", 640 | []), 641 | "format": (do_format, None, 642 | "", 643 | "Creates a new memory card image.", 644 | [opt("-c", "--clusters", type="int", 645 | help = "Size in clusters of the memory card."), 646 | opt("-f", "--overwrite-existing", action="store_true", 647 | help = "Overwrite any existing file"), 648 | opt("-e", "--no-ecc", action="store_true", 649 | help = "Create an image without ECC")]), 650 | "gui": (do_gui, None, 651 | "", 652 | "Starts the graphical user interface.", 653 | []), 654 | } 655 | 656 | # 657 | # secret commands for debugging purposes. 658 | # 659 | debug_cmd_table = { 660 | "frob": (do_frob, "r+b", 661 | "", 662 | None, 663 | []), 664 | "print_good_blocks": (do_print_good_blocks, "rb", 665 | "", 666 | None, 667 | []), 668 | "ecc_check": (do_ecc_check, "rb", 669 | "", 670 | None, 671 | []), 672 | "create_pad": (do_create_pad, "r+b", 673 | "", 674 | None, 675 | []) 676 | } 677 | 678 | del opt # clean up name space 679 | 680 | 681 | def write_error(filename, msg): 682 | if filename == None: 683 | sys.stderr.write(msg + "\n") 684 | else: 685 | sys.stderr.write(filename + ": " + msg + "\n") 686 | 687 | class suboption_parser(optparse.OptionParser): 688 | def exit(self, status = 0, msg = None): 689 | if msg: 690 | sys.stderr.write(msg) 691 | raise subopt_error, status 692 | 693 | class my_help_formatter(optparse.IndentedHelpFormatter): 694 | """A better formatter for optparser's help message""" 695 | 696 | def format_description(self, description): 697 | if not description: 698 | return "" 699 | desc_width = self.width - self.current_indent 700 | indent = " " * self.current_indent 701 | lines = [] 702 | for line in description.split('\n'): 703 | ii = indent 704 | si = indent 705 | if line.startswith("\t"): 706 | line = line[1:] 707 | ii = indent + " " * 4 708 | si = ii + " " * line.find(":") + 2 709 | line = textwrap.fill(line, desc_width, 710 | initial_indent = ii, 711 | subsequent_indent = si) 712 | lines.append(line) 713 | return "\n".join(lines) + "\n" 714 | 715 | def main(): 716 | prog = sys.argv[0].decode(sys.getdefaultencoding(), "replace") 717 | usage = "usage: %prog [-ih] memcard.ps2 command [...]" 718 | description = ("Manipulate PS2 memory card images.\n\n" 719 | "Supported commands: ") 720 | for cmd in sorted(cmd_table.keys()): 721 | description += "\n " + cmd + ": " + cmd_table[cmd][3] 722 | 723 | version = ("mymc " 724 | + verbuild.MYMC_VERSION_MAJOR 725 | + "." + verbuild.MYMC_VERSION_BUILD 726 | + " (" + _SCCS_ID + ")") 727 | 728 | optparser = optparse.OptionParser(prog = prog, usage = usage, 729 | description = description, 730 | version = version, 731 | formatter = my_help_formatter()) 732 | optparser.add_option("-D", dest = "debug", action = "store_true", 733 | default = False, help = optparse.SUPPRESS_HELP) 734 | optparser.add_option("-i", "--ignore-ecc", action = "store_true", 735 | help = "Ignore ECC errors while reading.") 736 | 737 | optparser.disable_interspersed_args() 738 | (opts, args) = optparser.parse_args() 739 | 740 | if len(args) == 0: 741 | try: 742 | import gui 743 | except ImportError: 744 | gui = None 745 | if gui != None: 746 | optparser.destroy() 747 | gui.run() 748 | sys.exit(0) 749 | 750 | if len(args) < 2: 751 | optparser.error("Incorrect number of arguments.") 752 | 753 | if opts.debug: 754 | cmd_table.update(debug_cmd_table) 755 | cmd = args[1] 756 | if cmd not in cmd_table: 757 | optparser.error('Command "%s" not recognized.' % cmd) 758 | (fn, mode, usage_args, description, optlist) = cmd_table[cmd] 759 | 760 | usage = "%prog" 761 | if len(optlist) > 0: 762 | usage += " [options]" 763 | if usage_args != None: 764 | usage += " " + usage_args 765 | subprog = prog + " memcard.ps2 " + cmd 766 | subopt_parser = suboption_parser(prog = subprog, usage = usage, 767 | description = description, 768 | option_list = optlist) 769 | subopt_parser.disable_interspersed_args() 770 | 771 | f = None 772 | mc = None 773 | ret = 0 774 | mcname = args[0] 775 | 776 | try: 777 | (subopts, subargs) = subopt_parser.parse_args(args[2:]) 778 | try: 779 | if mode == None: 780 | ret = fn(cmd, mcname, subopts, subargs, 781 | subopt_parser.error) 782 | else: 783 | f = file(mcname, mode) 784 | mc = ps2mc.ps2mc(f, opts.ignore_ecc) 785 | ret = fn(cmd, mc, subopts, subargs, 786 | subopt_parser.error) 787 | finally: 788 | if mc != None: 789 | mc.close() 790 | if f != None: 791 | # print "f.close()" 792 | f.close() 793 | 794 | except EnvironmentError, value: 795 | if getattr(value, "filename", None) != None: 796 | write_error(value.filename, value.strerror) 797 | ret = 1 798 | elif getattr(value, "strerror", None) != None: 799 | write_error(mcname, value.strerror) 800 | ret = 1 801 | else: 802 | # something weird 803 | raise 804 | if opts.debug: 805 | raise 806 | 807 | except subopt_error, (ret,): 808 | pass 809 | 810 | except (ps2mc.error, ps2save.error), value: 811 | fn = getattr(value, "filename", None) 812 | if fn == None: 813 | fn = mcname 814 | write_error(fn, str(value)) 815 | if opts.debug: 816 | raise 817 | ret = 1 818 | 819 | if ret == None: 820 | ret = 0 821 | 822 | return ret 823 | 824 | sys.exit(main()) 825 | 826 | -------------------------------------------------------------------------------- /ps2mc.py: -------------------------------------------------------------------------------- 1 | # 2 | # ps2mc.py 3 | # 4 | # By Ross Ridge 5 | # Public Domain 6 | # 7 | 8 | """Manipulate PS2 memory card images.""" 9 | 10 | _SCCS_ID = "@(#) mymc ps2mc.py 1.11 22/01/15 01:17:07\n" 11 | 12 | import sys 13 | import array 14 | import struct 15 | from errno import EACCES, ENOENT, EEXIST, ENOTDIR, EISDIR, EROFS, ENOTEMPTY,\ 16 | ENOSPC, EIO, EBUSY, EINVAL 17 | import fnmatch 18 | import traceback 19 | 20 | from round import * 21 | from ps2mc_ecc import * 22 | from ps2mc_dir import * 23 | import ps2save 24 | 25 | PS2MC_MAGIC = "Sony PS2 Memory Card Format " 26 | PS2MC_FAT_ALLOCATED_BIT = 0x80000000 27 | PS2MC_FAT_CHAIN_END = 0xFFFFFFFF 28 | PS2MC_FAT_CHAIN_END_UNALLOC = 0x7FFFFFFF 29 | PS2MC_FAT_CLUSTER_MASK = 0x7FFFFFFF 30 | PS2MC_MAX_INDIRECT_FAT_CLUSTERS = 32 31 | PS2MC_CLUSTER_SIZE = 1024 32 | PS2MC_INDIRECT_FAT_OFFSET = 0x2000 33 | 34 | PS2MC_STANDARD_PAGE_SIZE = 512 35 | PS2MC_STANDARD_PAGES_PER_CARD = 16384 36 | PS2MC_STANDARD_PAGES_PER_ERASE_BLOCK = 16 37 | 38 | class error(Exception): 39 | pass 40 | 41 | class io_error(error, IOError): 42 | def __init__(self, *args, **kwargs): 43 | IOError.__init__(self, *args, **kwargs) 44 | 45 | def __str__(self): 46 | if getattr(self, "strerror", None) == None: 47 | return str(self.args) 48 | if getattr(self, "filename", None) != None: 49 | return self.filename + ": " + self.strerror 50 | return self.strerror 51 | 52 | class path_not_found(io_error): 53 | def __init__(self, filename): 54 | io_error.__init__(self, ENOENT, "path not found", filename) 55 | 56 | class file_not_found(io_error): 57 | def __init__(self, filename): 58 | io_error.__init__(self, ENOENT, "file not found", filename) 59 | 60 | class dir_not_found(io_error): 61 | def __init__(self, filename): 62 | io_error.__init__(self, ENOENT, "directory not found", 63 | filename) 64 | 65 | class dir_index_not_found(io_error, IndexError): 66 | def __init__(self, filename, index): 67 | msg = "index (%d) past of end of directory" % index 68 | io_error.__init__(self, ENOENT, msg, filename) 69 | 70 | class corrupt(io_error): 71 | def __init__(self, msg, f = None): 72 | filename = None 73 | if f != None: 74 | filename = getattr(f, "name") 75 | io_error.__init__(self, EIO, msg, filename) 76 | 77 | class ecc_error(corrupt): 78 | def __init__(self, msg, filename = None): 79 | corrupt.__init__(self, msg, filename) 80 | 81 | if sys.byteorder == "big": 82 | def unpack_32bit_array(s): 83 | a = array.array('I', s) 84 | a.byteswap() 85 | return a 86 | 87 | def pack_32bit_array(a): 88 | a = a[:] 89 | a.byteswap() 90 | return a.tostring() 91 | else: 92 | def unpack_32bit_array(s): 93 | #if isinstance(s, str): 94 | # a = array.array('L') 95 | # a.fromstring(s) 96 | # return a 97 | return array.array('I', s) 98 | 99 | def pack_32bit_array(a): 100 | return a.tostring() 101 | 102 | def unpack_superblock(s): 103 | sb = struct.unpack("<28s12sHHHHLLLLLL8x128s128sbbxx", s) 104 | sb = list(sb) 105 | sb[12] = unpack_32bit_array(sb[12]) 106 | sb[13] = unpack_32bit_array(sb[13]) 107 | return sb 108 | 109 | def pack_superblock(sb): 110 | sb = list(sb) 111 | sb[12] = pack_32bit_array(sb[12]) 112 | sb[13] = pack_32bit_array(sb[13]) 113 | return struct.pack("<28s12sHHHHLLLLLL8x128s128sbbxx", *sb) 114 | 115 | unpack_fat = unpack_32bit_array 116 | pack_fat = pack_32bit_array 117 | 118 | def pathname_split(pathname): 119 | if pathname == "": 120 | return (None, False, False) 121 | components = pathname.split("/") 122 | return ([name 123 | for name in components 124 | if name != ""], 125 | components[0] != "", 126 | components[-1] == "") 127 | 128 | class lru_cache(object): 129 | def __init__(self, length): 130 | self._lru_list = [[i - 1, None, None, i + 1] 131 | for i in range(length + 1)] 132 | self._index_map = {} 133 | 134 | def dump(self): 135 | lru_list = self._lru_list 136 | i = 0 137 | while i != len(self._lru_list): 138 | print "%d: %s, " % (i, str(lru_list[i][1])), 139 | i = lru_list[i][3] 140 | print 141 | print self._index_map 142 | 143 | def _move_to_front(self, i): 144 | lru_list = self._lru_list 145 | first = lru_list[0] 146 | i2 = first[3] 147 | if i != i2: 148 | elt = lru_list[i] 149 | prev = lru_list[elt[0]] 150 | next = lru_list[elt[3]] 151 | prev[3] = elt[3] 152 | next[0] = elt[0] 153 | elt[0] = 0 154 | elt[3] = i2 155 | lru_list[i2][0] = i 156 | first[3] = i 157 | 158 | def add(self, key, value): 159 | lru_list = self._lru_list 160 | index_map = self._index_map 161 | ret = None 162 | if key in index_map: 163 | i = index_map[key] 164 | # print "add hit ", key, i 165 | elt = lru_list[i] 166 | else: 167 | # print "add miss", key 168 | i = lru_list[-1][0] 169 | elt = lru_list[i] 170 | old_key = elt[1] 171 | if old_key != None: 172 | del index_map[old_key] 173 | ret = (old_key, elt[2]) 174 | index_map[key] = i 175 | elt[1] = key 176 | elt[2] = value 177 | self._move_to_front(i) 178 | 179 | return ret 180 | 181 | def get(self, key, default = None): 182 | i = self._index_map.get(key) 183 | if i == None: 184 | # print "get miss", key 185 | return default 186 | # print "get hit ", key, i 187 | ret = self._lru_list[i][2] 188 | self._move_to_front(i) 189 | return ret 190 | 191 | def items(self): 192 | return [(elt[1], elt[2]) 193 | for elt in self._lru_list[1 : -1] 194 | if elt[2] != None] 195 | 196 | class fat_chain(object): 197 | """A class for accessing a file's FAT entries as a simple sequence.""" 198 | 199 | def __init__(self, lookup_fat, first): 200 | self.lookup_fat = lookup_fat 201 | self._first = first 202 | self.offset = 0 203 | self._prev = None 204 | self._cur = first 205 | 206 | def __getitem__(self, i): 207 | # not iterable 208 | offset = self.offset 209 | if i == offset: 210 | # print "@@@ fat_chain[] cur:", i, self._cur 211 | return self._cur 212 | elif i == offset - 1: 213 | assert self._prev != None 214 | # print "@@@ fat_chain[] prev:", i, self._prev 215 | return self._prev 216 | if i < offset: 217 | if i == 0: 218 | # print "@@@ fat_chain[] first", i, self._first 219 | return self._first 220 | offset = 0 221 | prev = None 222 | cur = self._first 223 | else: 224 | prev = self._prev 225 | cur = self._cur 226 | # print "@@@ fat_chain[] distance", i - offset 227 | while offset != i: 228 | next = self.lookup_fat(cur) 229 | if next == PS2MC_FAT_CHAIN_END: 230 | break; 231 | if next & PS2MC_FAT_ALLOCATED_BIT: 232 | next &= ~PS2MC_FAT_ALLOCATED_BIT 233 | else: 234 | # corrupt 235 | next = PS2MC_FAT_CHAIN_END 236 | break 237 | 238 | offset += 1 239 | prev = cur 240 | cur = next 241 | self.offset = offset 242 | self._prev = prev 243 | self._cur = cur 244 | # print "@@@ offset, prev, cur:", offset, prev, cur 245 | # print "@@@ fat_chain[]", i, next 246 | return next 247 | 248 | def __len__(self): 249 | old_prev = self._prev 250 | old_cur = self._cur 251 | old_offset = self.offset 252 | i = self.offset 253 | while self[i] != PS2MC_FAT_CHAIN_END: 254 | i += 1 255 | self._prev = old_prev 256 | self._cur = old_cur 257 | self.offset = old_offset 258 | return i 259 | 260 | class ps2mc_file(object): 261 | """A file-like object for accessing a file in memory card image.""" 262 | 263 | def __init__(self, mc, dirloc, first_cluster, length, mode, 264 | name = None): 265 | # print "ps2mc_file.__init__", name, self 266 | self.mc = mc 267 | self.length = length 268 | self.first_cluster = first_cluster 269 | self.dirloc = dirloc 270 | self.fat_chain = None 271 | self._pos = 0 272 | self.buffer = None 273 | self.buffer_cluster = None 274 | self.softspace = 0 275 | if name == None: 276 | self.name = "" 277 | else: 278 | self.name = name 279 | self.closed = False 280 | 281 | if mode == None or len(mode) == 0: 282 | mode = "rb" 283 | self.mode = mode 284 | self._append = False 285 | self._write = False 286 | if mode[0] == "a": 287 | self._append = True 288 | elif mode[0] != "w" or ("+" not in self.mode): 289 | self._write = True 290 | 291 | def _find_file_cluster(self, n): 292 | if self.fat_chain == None: 293 | self.fat_chain = self.mc.fat_chain(self.first_cluster) 294 | return self.fat_chain[n] 295 | 296 | def read_file_cluster(self, n): 297 | if n == self.buffer_cluster: 298 | return self.buffer 299 | cluster = self._find_file_cluster(n) 300 | # print "@@@ read_file_cluster", self.dirloc, n, cluster, repr(self.name) 301 | if cluster == PS2MC_FAT_CHAIN_END: 302 | return None 303 | self.buffer = self.mc.read_allocatable_cluster(cluster) 304 | self.buffer_cluster = n 305 | return self.buffer 306 | 307 | def _extend_file(self, n): 308 | mc = self.mc 309 | cluster = mc.allocate_cluster() 310 | # print "@@@ extending file", n, cluster 311 | if cluster == None: 312 | return None 313 | if n == 0: 314 | self.first_cluster = cluster 315 | self.fat_chain = None 316 | # print "@@@ linking", self.dirloc, "->", cluster 317 | mc.update_dirent(self.dirloc, self, cluster, 318 | None, False) 319 | else: 320 | prev = self.fat_chain[n - 1] 321 | # print "@@@ linking", prev, "->", cluster 322 | mc.set_fat(prev, cluster | PS2MC_FAT_ALLOCATED_BIT) 323 | return cluster 324 | 325 | def write_file_cluster(self, n, buf): 326 | mc = self.mc 327 | cluster = self._find_file_cluster(n) 328 | if cluster != PS2MC_FAT_CHAIN_END: 329 | mc.write_allocatable_cluster(cluster, buf) 330 | self.buffer = buf 331 | self.buffer_cluster = n 332 | return True 333 | 334 | cluster_size = mc.cluster_size 335 | file_cluster_end = div_round_up(self.length, cluster_size) 336 | 337 | if (cluster < file_cluster_end 338 | or len(self.fat_chain) != file_cluster_end): 339 | raise corrupt, ("file length doesn't match cluster" 340 | " chain length", mc.f) 341 | 342 | for i in range(file_cluster_end, n): 343 | cluster = self._extend_file(i) 344 | if cluster == None: 345 | if i != file_cluster_end: 346 | self.length = (i - 1) * cluster_size 347 | mc.update_dirent(self.dirloc, self, 348 | None, self.length, 349 | True) 350 | return False 351 | mc.write_allocatable_cluster(cluster, 352 | ["\0"] * cluster_size) 353 | 354 | cluster = self._extend_file(n) 355 | if cluster == None: 356 | return False 357 | 358 | mc.write_allocatable_cluster(cluster, buf) 359 | self.buffer = buf 360 | self.buffer_cluster = n 361 | return True 362 | 363 | def update_notify(self, first_cluster, length): 364 | if self.first_cluster != first_cluster: 365 | self.first_cluster = first_cluster 366 | self.fat_chain = None 367 | self.length = length 368 | self.buffer = None 369 | self.buffer_cluster = None 370 | 371 | def read(self, size = None, eol = None): 372 | if self.closed: 373 | raise ValueError, "file is closed" 374 | 375 | pos = self._pos 376 | cluster_size = self.mc.cluster_size 377 | if size == None: 378 | size = self.length 379 | size = max(min(self.length - pos, size), 0) 380 | ret = "" 381 | while size > 0: 382 | off = pos % cluster_size 383 | l = min(cluster_size - off, size) 384 | buf = self.read_file_cluster(pos / cluster_size) 385 | if buf == None: 386 | break 387 | if eol != None: 388 | i = buf.find(eol, off, off + l) 389 | if i != -1: 390 | l = off - i + 1 391 | size = l 392 | pos += l 393 | self._pos = pos 394 | ret += buf[off : off + l] 395 | size -= l 396 | return ret 397 | 398 | def write(self, out, _set_modified = True): 399 | if self.closed: 400 | raise ValueError, "file is closed" 401 | 402 | cluster_size = self.mc.cluster_size 403 | pos = self._pos 404 | if self._append: 405 | pos = self.length 406 | elif not self._write: 407 | raise io_error, (EACCES, "file not opened for writing", 408 | self.name) 409 | 410 | size = len(out) 411 | # print "@@@ write", pos, size 412 | i = 0 413 | while size > 0: 414 | cluster = pos / cluster_size 415 | off = pos % cluster_size 416 | l = min(cluster_size - off, size) 417 | s = out[i : i + l] 418 | pos += l 419 | if l == cluster_size: 420 | buf = s 421 | else: 422 | buf = self.read_file_cluster(cluster) 423 | if buf == None: 424 | buf = "\0" * cluster_size 425 | buf = buf[:off] + s + buf[off + l:] 426 | if not self.write_file_cluster(cluster, buf): 427 | raise io_error, (ENOSPC, 428 | "out of space on image", 429 | self.name) 430 | self._pos = pos 431 | # print "@@@ pos", pos 432 | new_length = None 433 | if pos > self.length: 434 | new_length = self.length = pos 435 | self.mc.update_dirent(self.dirloc, self, None, 436 | new_length, _set_modified) 437 | 438 | i += l 439 | size -= l 440 | 441 | def close(self): 442 | # print "ps2mc_file.close", self.name, self 443 | if self.mc != None: 444 | self.mc.notify_closed(self.dirloc, self) 445 | self.mc = None 446 | self.fat_chain = None 447 | self.buffer = None 448 | 449 | def next(self): 450 | r = self.readline() 451 | if r == "": 452 | raise StopIteration 453 | return r 454 | 455 | def readline(self, size = None): 456 | return self.read(size, "\n") 457 | 458 | def readlines(self, sizehint): 459 | return [line for line in self] 460 | 461 | def seek(self, offset, whence = 0): 462 | if self.closed: 463 | raise ValueError, "file is closed" 464 | 465 | if whence == 1: 466 | base = self._pos 467 | elif whence == 2: 468 | base = self.length 469 | else: 470 | base = 0 471 | pos = max(base + offset, 0) 472 | self._pos = pos 473 | 474 | def tell(self): 475 | if self.closed: 476 | raise ValueError, "file is closed" 477 | return self._pos 478 | 479 | def __enter__(self): 480 | return 481 | 482 | def __exit__(self, a, b, c): 483 | self.close() 484 | return 485 | 486 | # def __del__(self): 487 | # # print "ps2mc_file.__del__", self 488 | # if self.mc != None: 489 | # self.mc.notify_closed(self.dirloc, self) 490 | # self.mc = None 491 | # self.fat_chain = None 492 | 493 | class ps2mc_directory(object): 494 | """A sequence and iterator object for directories.""" 495 | 496 | def __init__(self, mc, dirloc, first_cluster, length, 497 | mode = "rb", name = None): 498 | self.f = ps2mc_file(mc, dirloc, first_cluster, 499 | length * PS2MC_DIRENT_LENGTH, mode, name) 500 | 501 | def __iter__(self): 502 | start = self.tell() 503 | if start != 0: 504 | start -= 1 505 | self.seek(start) 506 | self._iter_end = start 507 | return self 508 | 509 | def write_raw_ent(self, index, ent, set_modified): 510 | # print "@@@ write_raw_ent", index 511 | self.seek(index) 512 | self.f.write(pack_dirent(ent), 513 | _set_modified = set_modified) 514 | 515 | def next(self): 516 | # print "@@@ next", self.tell(), self.f.name 517 | dirent = self.f.read(PS2MC_DIRENT_LENGTH) 518 | if dirent == "": 519 | if 0 == self._iter_end: 520 | raise StopIteration 521 | self.seek(0) 522 | dirent = self.f.read(PS2MC_DIRENT_LENGTH) 523 | elif self.tell() == self._iter_end: 524 | raise StopIteration 525 | return unpack_dirent(dirent) 526 | 527 | def seek(self, offset, whence = 0): 528 | self.f.seek(offset * PS2MC_DIRENT_LENGTH, whence) 529 | 530 | def tell(self): 531 | return self.f.tell() / PS2MC_DIRENT_LENGTH 532 | 533 | def __len__(self): 534 | return self.f.length / PS2MC_DIRENT_LENGTH 535 | 536 | def __getitem__(self, index): 537 | # print "@@@ getitem", index, self.f.name 538 | self.seek(index) 539 | dirent = self.f.read(PS2MC_DIRENT_LENGTH) 540 | if len(dirent) != PS2MC_DIRENT_LENGTH: 541 | raise dir_index_not_found(self.f.name, index) 542 | return unpack_dirent(dirent) 543 | 544 | def __setitem__(self, index, new_ent): 545 | ent = self[index] 546 | mode = ent[0] 547 | if (mode & DF_EXISTS) == 0: 548 | return 549 | if new_ent[0] != None: 550 | mode = ((new_ent[0] & ~(DF_FILE | DF_DIR | DF_EXISTS)) 551 | | (mode & (DF_FILE | DF_DIR | DF_EXISTS))) 552 | ent[0] = mode 553 | for i in [1, 3, 6, 7, 8]: # ???, created, modifed, attr 554 | if new_ent[i] != None: 555 | ent[i] = new_ent[i] 556 | self.write_raw_ent(index, ent, False) 557 | 558 | def close(self): 559 | # print "ps2mc_directory.close", self 560 | self.f.close() 561 | self.f = None 562 | 563 | def __del__(self): 564 | # print "ps2mc_directory.__del__", self 565 | if self.f != None: 566 | self.f.close() 567 | self.f = None 568 | 569 | class _root_directory(ps2mc_directory): 570 | """Wrapper for the cached root directory object. 571 | 572 | The close() method is disabled so the cached object can be reused.""" 573 | 574 | def __init__(self, mc, dirloc, first_cluster, length, 575 | mode = "r+b", name = "/"): 576 | ps2mc_directory.__init__(self, mc, dirloc, first_cluster, 577 | length, mode, name) 578 | 579 | def close(self): 580 | pass 581 | 582 | def real_close(self): 583 | ps2mc_directory.close(self) 584 | 585 | class ps2mc(object): 586 | """A PlayStation 2 memory card filesystem implementation. 587 | 588 | The close() method must be called when the object is no longer needed, 589 | otherwise cycles that can't be collected by the garbage collector 590 | will remain.""" 591 | 592 | open_files = None 593 | fat_cache = None 594 | 595 | def _calculate_derived(self): 596 | self.spare_size = div_round_up(self.page_size, 128) * 4 597 | self.raw_page_size = self.page_size + self.spare_size 598 | self.cluster_size = self.page_size * self.pages_per_cluster 599 | self.entries_per_cluster = (self.page_size 600 | * self.pages_per_cluster / 4) 601 | 602 | limit = (min(self.good_block2, self.good_block1) 603 | * self.pages_per_erase_block 604 | / self.pages_per_cluster 605 | - self.allocatable_cluster_offset) 606 | self.allocatable_cluster_limit = limit 607 | 608 | def __init__(self, f, ignore_ecc = False, params = None): 609 | self.open_files = {} 610 | self.fat_cache = lru_cache(12) 611 | self.alloc_cluster_cache = lru_cache(64) 612 | self.modified = False 613 | self.f = None 614 | self.rootdir = None 615 | 616 | f.seek(0) 617 | s = f.read(0x154) 618 | if len(s) != 0x154 or not s.startswith(PS2MC_MAGIC): 619 | if (params == None): 620 | raise corrupt, ("Not a PS2 memory card image", 621 | f) 622 | self.f = f 623 | self.format(params) 624 | else: 625 | sb = unpack_superblock(s) 626 | self.version = sb[1] 627 | self.page_size = sb[2] 628 | self.pages_per_cluster = sb[3] 629 | self.pages_per_erase_block = sb[4] 630 | self.clusters_per_card = sb[6] 631 | self.allocatable_cluster_offset = sb[7] 632 | self.allocatable_cluster_end = sb[8] 633 | self.rootdir_fat_cluster = sb[9] 634 | self.good_block1 = sb[10] 635 | self.good_block2 = sb[11] 636 | self.indirect_fat_cluster_list = sb[12] 637 | self.bad_erase_block_list = sb[13] 638 | 639 | self._calculate_derived() 640 | 641 | self.f = f 642 | self.ignore_ecc = False 643 | 644 | try: 645 | self.read_page(0) 646 | self.ignore_ecc = ignore_ecc 647 | except ecc_error: 648 | # the error might be due the fact the file 649 | # image doesn't contain ECC data 650 | self.spare_size = 0 651 | self.raw_page_size = self.page_size 652 | ignore_ecc = True 653 | 654 | # sanity check 655 | root = self._directory(None, 0, 1) 656 | dot = root[0] 657 | dotdot = root[1] 658 | root.close() 659 | if (dot[8] != "." or dotdot[8] != ".." 660 | or not mode_is_dir(dot[0]) or not mode_is_dir(dotdot[0])): 661 | raise corrupt, "Root directory damaged." 662 | 663 | self.fat_cursor = 0 664 | self.curdir = (0, 0) 665 | 666 | def write_superblock(self): 667 | s = pack_superblock((PS2MC_MAGIC, 668 | self.version, 669 | self.page_size, 670 | self.pages_per_cluster, 671 | self.pages_per_erase_block, 672 | 0xFF00, 673 | self.clusters_per_card, 674 | self.allocatable_cluster_offset, 675 | self.allocatable_cluster_end, 676 | self.rootdir_fat_cluster, 677 | self.good_block1, 678 | self.good_block2, 679 | self.indirect_fat_cluster_list, 680 | self.bad_erase_block_list, 681 | 2, 682 | 0x2B)) 683 | s += "\x00" * (self.page_size - len(s)) 684 | self.write_page(0, s) 685 | 686 | page = "\xFF" * self.raw_page_size 687 | self.f.seek(self.good_block2 * self.pages_per_erase_block 688 | * self.raw_page_size) 689 | for i in range(self.pages_per_erase_block): 690 | self.f.write(page) 691 | 692 | self.modified = False 693 | return 694 | 695 | def format(self, params): 696 | """Create (format) a new memory card image.""" 697 | 698 | (with_ecc, page_size, 699 | pages_per_erase_block, param_pages_per_card) = params 700 | 701 | if pages_per_erase_block < 1: 702 | raise error, ("invalid pages per erase block (%d)" 703 | % page_size) 704 | 705 | pages_per_card = round_down(param_pages_per_card, 706 | pages_per_erase_block) 707 | cluster_size = PS2MC_CLUSTER_SIZE 708 | pages_per_cluster = cluster_size / page_size 709 | clusters_per_erase_block = (pages_per_erase_block 710 | / pages_per_cluster) 711 | erase_blocks_per_card = pages_per_card / pages_per_erase_block 712 | clusters_per_card = pages_per_card / pages_per_cluster 713 | epc = cluster_size / 4 714 | 715 | if (page_size < PS2MC_DIRENT_LENGTH 716 | or pages_per_cluster < 1 717 | or pages_per_cluster * page_size != cluster_size): 718 | raise error, "invalid page size (%d)" % page_size 719 | 720 | good_block1 = erase_blocks_per_card - 1 721 | good_block2 = erase_blocks_per_card - 2 722 | first_ifc = div_round_up(PS2MC_INDIRECT_FAT_OFFSET, 723 | cluster_size) 724 | 725 | allocatable_clusters = clusters_per_card - (first_ifc + 2) 726 | fat_clusters = div_round_up(allocatable_clusters, epc) 727 | indirect_fat_clusters = div_round_up(fat_clusters, epc) 728 | if indirect_fat_clusters > PS2MC_MAX_INDIRECT_FAT_CLUSTERS: 729 | indirect_fat_clusters = PS2MC_MAX_INDIRECT_FAT_CLUSTERS 730 | fat_clusters = indirect_fat_clusters * epc 731 | allocatable_clusters = fat_clusters * epc 732 | 733 | allocatable_cluster_offset = (first_ifc 734 | + indirect_fat_clusters 735 | + fat_clusters) 736 | allocatable_cluster_end = (good_block2 737 | * clusters_per_erase_block 738 | - allocatable_cluster_offset) 739 | if allocatable_cluster_end < 1: 740 | raise error, ("memory card image too small" 741 | " to be formatted") 742 | 743 | ifc_list = unpack_fat("\0\0\0\0" 744 | * PS2MC_MAX_INDIRECT_FAT_CLUSTERS) 745 | for i in range(indirect_fat_clusters): 746 | ifc_list[i] = first_ifc + i 747 | 748 | self.version = "1.2.0.0" 749 | self.page_size = page_size 750 | self.pages_per_cluster = pages_per_cluster 751 | self.pages_per_erase_block = pages_per_erase_block 752 | self.clusters_per_card = clusters_per_card 753 | self.allocatable_cluster_offset = allocatable_cluster_offset 754 | self.allocatable_cluster_end = allocatable_clusters 755 | self.rootdir_fat_cluster = 0 756 | self.good_block1 = good_block1 757 | self.good_block2 = good_block2 758 | self.indirect_fat_cluster_list = ifc_list 759 | bebl = "\xFF\xFF\xFF\xFF" * 32 760 | self.bad_erase_block_list = unpack_32bit_array(bebl) 761 | 762 | self._calculate_derived() 763 | 764 | self.ignore_ecc = not with_ecc 765 | erased = "\0" * page_size 766 | if not with_ecc: 767 | self.spare_size = 0 768 | else: 769 | ecc = "".join(["".join(map(chr, s)) 770 | for s in ecc_calculate_page(erased)]) 771 | erased += ecc + "\0" * (self.spare_size - len(ecc)) 772 | 773 | self.f.seek(0) 774 | for page in range(pages_per_card): 775 | self.f.write(erased) 776 | 777 | self.modified = True 778 | 779 | first_fat_cluster = first_ifc + indirect_fat_clusters 780 | remainder = fat_clusters % epc 781 | for i in range(indirect_fat_clusters): 782 | base = first_fat_cluster + i * epc 783 | buf = unpack_fat(range(base, base + epc)) 784 | if (i == indirect_fat_clusters - 1 785 | and remainder != 0): 786 | del buf[remainder:] 787 | buf.fromlist([0xFFFFFFFF] * (epc - remainder)) 788 | self._write_fat_cluster(ifc_list[i], buf) 789 | 790 | 791 | # go through the fat backwards for better cache usage 792 | for i in range(allocatable_clusters - 1, 793 | allocatable_cluster_end - 1, -1): 794 | self.set_fat(i, PS2MC_FAT_CHAIN_END) 795 | for i in range(allocatable_cluster_end - 1, 0, -1): 796 | self.set_fat(i, PS2MC_FAT_CLUSTER_MASK) 797 | self.set_fat(0, PS2MC_FAT_CHAIN_END) 798 | 799 | self.allocatable_cluster_end = allocatable_cluster_end 800 | 801 | now = tod_now() 802 | s = pack_dirent((DF_RWX | DF_DIR | DF_0400 | DF_EXISTS, 803 | 0, 2, now, 804 | 0, 0, now, 0, ".")) 805 | s += "\0" * (cluster_size - len(s)) 806 | self.write_allocatable_cluster(0, s) 807 | dir = self._directory((0, 0), 0, 2, "wb", "/") 808 | dir.write_raw_ent(1, (DF_WRITE | DF_EXECUTE | DF_DIR | DF_0400 809 | | DF_HIDDEN | DF_EXISTS, 810 | 0, 0, now, 811 | 0, 0, now, 0, ".."), False) 812 | dir.close() 813 | 814 | self.flush() 815 | 816 | def read_page(self, n): 817 | # print "@@@ page", n 818 | f = self.f 819 | f.seek(self.raw_page_size * n) 820 | page = f.read(self.page_size) 821 | if len(page) != self.page_size: 822 | raise corrupt, ("attempted to read past EOF" 823 | " (page %05X)" % n, f) 824 | if self.ignore_ecc: 825 | return page 826 | spare = f.read(self.spare_size) 827 | if len(spare) != self.spare_size: 828 | raise corrupt, ("attempted to read past EOF" 829 | " (page %05X)" % n, f) 830 | (status, page, spare) = ecc_check_page(page, spare) 831 | if status == ECC_CHECK_FAILED: 832 | raise ecc_error, ("Unrecoverable ECC error (page %d)" 833 | % n) 834 | return page 835 | 836 | def write_page(self, n, buf): 837 | f = self.f 838 | f.seek(self.raw_page_size * n) 839 | self.modified = True 840 | if len(buf) != self.page_size: 841 | raise error, ("internal error: write_page:" 842 | " %d != %d" % (len(buf), self.page_size)) 843 | f.write(buf) 844 | if self.spare_size != 0: 845 | a = array.array('B') 846 | for s in ecc_calculate_page(buf): 847 | a.fromlist(s) 848 | a.tofile(f) 849 | f.write("\0" * (self.spare_size - len(a))) 850 | 851 | def read_cluster(self, n): 852 | pages_per_cluster = self.pages_per_cluster 853 | cluster_size = self.cluster_size 854 | if self.spare_size == 0: 855 | self.f.seek(cluster_size * n) 856 | return self.f.read(cluster_size) 857 | n *= pages_per_cluster 858 | if pages_per_cluster == 2: 859 | return self.read_page(n) + self.read_page(n + 1) 860 | return "".join(map(self.read_page, 861 | range(n, n + pages_per_cluster))) 862 | 863 | def write_cluster(self, n, buf): 864 | pages_per_cluster = self.pages_per_cluster 865 | cluster_size = self.cluster_size 866 | if self.spare_size == 0: 867 | self.f.seek(cluster_size * n) 868 | if len(buf) != cluster_size: 869 | raise error, ("internal error: write_cluster:" 870 | " %d != %d" % (len(buf), 871 | cluster_size)) 872 | return self.f.write(buf) 873 | n *= pages_per_cluster 874 | pgsize = self.page_size 875 | for i in range(pages_per_cluster): 876 | self.write_page(n + i, buf[i * pgsize 877 | : i * pgsize + pgsize]) 878 | 879 | 880 | def _add_fat_cluster_to_cache(self, n, fat, dirty): 881 | old = self.fat_cache.add(n, [fat, dirty]) 882 | if old != None: 883 | (n, [fat, dirty]) = old 884 | if dirty: 885 | self.write_cluster(n, pack_fat(fat)) 886 | 887 | def _read_fat_cluster(self, n): 888 | v = self.fat_cache.get(n) 889 | if v != None: 890 | # print "@@@ fat hit", n 891 | return v[0] 892 | # print "@@@ fat miss", n 893 | fat = unpack_fat(self.read_cluster(n)) 894 | self._add_fat_cluster_to_cache(n, fat, False) 895 | return fat 896 | 897 | def _write_fat_cluster(self, n, fat): 898 | self._add_fat_cluster_to_cache(n, fat, True) 899 | 900 | def flush_fat_cache(self): 901 | if self.fat_cache == None: 902 | return 903 | for (n, v) in self.fat_cache.items(): 904 | [fat, dirty] = v 905 | if dirty: 906 | self.write_cluster(n, pack_fat(fat)) 907 | v[1] = False 908 | 909 | def _add_alloc_cluster_to_cache(self, n, buf, dirty): 910 | old = self.alloc_cluster_cache.add(n, [buf, dirty]) 911 | if old != None: 912 | (n, [buf, dirty]) = old 913 | if dirty: 914 | n += self.allocatable_cluster_offset 915 | self.write_cluster(n, buf) 916 | 917 | def read_allocatable_cluster(self, n): 918 | a = self.alloc_cluster_cache.get(n) 919 | if a != None: 920 | # print "@@@ cache hit", n 921 | return a[0] 922 | # print "@@@ cache miss", n 923 | buf = self.read_cluster(n + self.allocatable_cluster_offset) 924 | self._add_alloc_cluster_to_cache(n, buf, False) 925 | return buf 926 | 927 | def write_allocatable_cluster(self, n, buf): 928 | self._add_alloc_cluster_to_cache(n, buf, True) 929 | 930 | def flush_alloc_cluster_cache(self): 931 | if self.alloc_cluster_cache == None: 932 | return 933 | for (n, a) in self.alloc_cluster_cache.items(): 934 | [buf, dirty] = a 935 | if dirty: 936 | n += self.allocatable_cluster_offset 937 | self.write_cluster(n, buf) 938 | a[1] = False 939 | 940 | def read_fat_cluster(self, n): 941 | indirect_offset = n % self.entries_per_cluster 942 | dbl_offset = n / self.entries_per_cluster 943 | indirect_cluster = self.indirect_fat_cluster_list[dbl_offset] 944 | indirect_fat = self._read_fat_cluster(indirect_cluster) 945 | cluster = indirect_fat[indirect_offset] 946 | return (self._read_fat_cluster(cluster), cluster) 947 | 948 | def read_fat(self, n): 949 | if n < 0 or n >= self.allocatable_cluster_end: 950 | raise io_error, (EIO, 951 | "FAT cluster index out of range" 952 | " (%d)" % n) 953 | offset = n % self.entries_per_cluster 954 | fat_cluster = n / self.entries_per_cluster 955 | (fat, cluster) = self.read_fat_cluster(fat_cluster) 956 | return (fat, offset, cluster) 957 | 958 | def lookup_fat(self, n): 959 | (fat, offset, cluster) = self.read_fat(n) 960 | return fat[offset] 961 | 962 | def set_fat(self, n, value): 963 | (fat, offset, cluster) = self.read_fat(n) 964 | fat[offset] = value 965 | self._write_fat_cluster(cluster, fat) 966 | 967 | def allocate_cluster(self): 968 | epc = self.entries_per_cluster 969 | allocatable_cluster_limit = self.allocatable_cluster_limit 970 | 971 | end = div_round_up(allocatable_cluster_limit, epc) 972 | remainder = allocatable_cluster_limit % epc 973 | 974 | while self.fat_cursor < end: 975 | (fat, cluster) = self.read_fat_cluster(self.fat_cursor) 976 | if (self.fat_cursor == end - 1 977 | and remainder != 0): 978 | n = min(fat[:remainder]) 979 | else: 980 | n = min(fat) 981 | if (n & PS2MC_FAT_ALLOCATED_BIT) == 0: 982 | offset = fat.index(n) 983 | fat[offset] = PS2MC_FAT_CHAIN_END 984 | self._write_fat_cluster(cluster, fat) 985 | ret = self.fat_cursor * epc + offset 986 | # print "@@@ allocated", ret 987 | return ret 988 | self.fat_cursor += 1 989 | return None 990 | 991 | def fat_chain(self, first_cluster): 992 | return fat_chain(self.lookup_fat, first_cluster) 993 | 994 | def file(self, dirloc, first_cluster, length, mode, name = None): 995 | """Create a new file-like object for a file.""" 996 | 997 | f = ps2mc_file(self, dirloc, first_cluster, length, mode, name) 998 | if dirloc == None: 999 | return 1000 | open_files = self.open_files 1001 | if dirloc not in open_files: 1002 | open_files[dirloc] = [None, set([f])] 1003 | else: 1004 | open_files[dirloc][1].add(f) 1005 | return f 1006 | 1007 | def directory(self, dirloc, first_cluster, length, 1008 | mode = None, name = None): 1009 | return ps2mc_directory(self, dirloc, first_cluster, length, 1010 | mode, name) 1011 | 1012 | def _directory(self, dirloc, first_cluster, length, 1013 | mode = None, name = None): 1014 | # print "@@@ _directory", dirloc, first_cluster, length 1015 | if first_cluster != 0: 1016 | return self.directory(dirloc, first_cluster, length, 1017 | mode, name) 1018 | if dirloc == None: 1019 | dirloc = (0, 0) 1020 | assert dirloc == (0, 0) 1021 | if self.rootdir != None: 1022 | return self.rootdir 1023 | dir = _root_directory(self, dirloc, 0, length, "r+b", "/") 1024 | l = dir[0][2] 1025 | if l != length: 1026 | dir.real_close() 1027 | dir = _root_directory(self, dirloc, 0, l, "r+b", "/") 1028 | self.rootdir = dir 1029 | return dir 1030 | 1031 | def _get_parent_dirloc(self, dirloc): 1032 | """Get the dirloc of the parent directory of the 1033 | file or directory refered to by dirloc""" 1034 | 1035 | cluster = self.read_allocatable_cluster(dirloc[0]) 1036 | ent = unpack_dirent(cluster[:PS2MC_DIRENT_LENGTH]) 1037 | return (ent[4], ent[5]) 1038 | 1039 | def _dirloc_to_ent(self, dirloc): 1040 | """Get the directory entry of the file or directory 1041 | refered to by dirloc""" 1042 | 1043 | dir = self._directory(None, dirloc[0], dirloc[1] + 1, 1044 | name = "_dirloc_to_ent temp") 1045 | ent = dir[dirloc[1]] 1046 | dir.close() 1047 | return ent 1048 | 1049 | def _opendir_dirloc(self, dirloc, mode = "rb"): 1050 | """Open the directory that is refered to by dirloc""" 1051 | 1052 | ent = self._dirloc_to_ent(dirloc) 1053 | return self._directory(dirloc, ent[4], ent[2], 1054 | name = "_opendir_dirloc temp") 1055 | 1056 | def _opendir_parent_dirloc(self, dirloc, mode = "rb"): 1057 | """Open the directory that contains the file or directory 1058 | refered to by dirloc""" 1059 | 1060 | return self._opendir_dirloc(self._get_parent_dirloc(dirloc), 1061 | mode) 1062 | 1063 | def update_dirent_all(self, dirloc, thisf, new_ent): 1064 | # print "@@@ update_dirent", dirloc 1065 | # print "@@@ new_ent", new_ent 1066 | opened = self.open_files.get(dirloc, None) 1067 | if opened == None: 1068 | files = [] 1069 | dir = None 1070 | else: 1071 | dir, files = opened 1072 | if dir == None: 1073 | dir = self._opendir_parent_dirloc(dirloc, "r+b") 1074 | if opened != None: 1075 | opened[0] = dir 1076 | 1077 | ent = dir[dirloc[1]] 1078 | # print "@@@ old_ent", ent 1079 | 1080 | is_dir = ent[0] & DF_DIR 1081 | 1082 | if is_dir and thisf != None and new_ent[2] != None: 1083 | new_ent = list(new_ent) 1084 | new_ent[2] /= PS2MC_DIRENT_LENGTH 1085 | 1086 | # print "len: ", ent[2], new_ent[2] 1087 | 1088 | modified = changed = notify = False 1089 | for i in range(len(ent)): 1090 | new = new_ent[i] 1091 | if new != None: 1092 | if new != ent[i]: 1093 | ent[i] = new 1094 | changed = True 1095 | if i == 6: 1096 | modified = True 1097 | if i in [2, 4]: 1098 | notify = True 1099 | 1100 | # Modifying a file causes the modification time of 1101 | # both the file and the file's directory to updated, 1102 | # however modifying a directory never updates the 1103 | # modification time of the directory's parent. 1104 | if changed: 1105 | dir.write_raw_ent(dirloc[1], ent, 1106 | (modified and not is_dir)) 1107 | 1108 | 1109 | if notify: 1110 | for f in files: 1111 | if f != thisf: 1112 | f.update_notfiy(ent[4], ent[2]) 1113 | if opened == None: 1114 | dir.close() 1115 | 1116 | def update_dirent(self, dirloc, thisf, first_cluster, length, 1117 | modified): 1118 | if modified: 1119 | modified = tod_now() 1120 | else: 1121 | if first_cluster == None and length == None: 1122 | return 1123 | modified = None 1124 | self.update_dirent_all(dirloc, thisf, 1125 | (None, None, length, None, 1126 | first_cluster, None, modified, None, 1127 | None)) 1128 | 1129 | def notify_closed(self, dirloc, thisf): 1130 | if self.open_files == None or dirloc == None: 1131 | return 1132 | a = self.open_files.get(dirloc, None) 1133 | if a == None: 1134 | return 1135 | self.flush() 1136 | dir, files = a 1137 | files.discard(thisf) 1138 | if len(files) == 0: 1139 | # print "@@@ notify_closed", dir 1140 | if dir != None: 1141 | dir.close() 1142 | del self.open_files[dirloc] 1143 | 1144 | def search_directory(self, dir, name): 1145 | """Search dir for name.""" 1146 | 1147 | # start the search where the last search ended. 1148 | start = dir.tell() - 1 1149 | if start == -1: 1150 | start = 0 1151 | for i in range(start, len(dir)) + range(0, start): 1152 | try: 1153 | ent = dir[i] 1154 | except IndexError: 1155 | raise corrupt("Corrupt directory", dir.f) 1156 | 1157 | if ent[8] == name and (ent[0] & DF_EXISTS): 1158 | return (i, ent) 1159 | return (None, None) 1160 | 1161 | def create_dir_entry(self, parent_dirloc, name, mode): 1162 | """Create a new directory entry in a directory.""" 1163 | 1164 | if name == "": 1165 | raise file_not_found, name 1166 | 1167 | # print "@@@ create_dir_ent", parent_dirloc, name 1168 | dir_ent = self._dirloc_to_ent(parent_dirloc) 1169 | dir = self._directory(parent_dirloc, dir_ent[4], dir_ent[2], 1170 | "r+b") 1171 | l = len(dir) 1172 | # print "@@@ len", l 1173 | assert l >= 2 1174 | for i in range(l): 1175 | ent = dir[i] 1176 | if (ent[0] & DF_EXISTS) == 0: 1177 | break 1178 | else: 1179 | i = l 1180 | 1181 | dirloc = (dir_ent[4], i) 1182 | # print "@@@ dirloc", dirloc 1183 | now = tod_now() 1184 | if mode & DF_DIR: 1185 | mode &= ~DF_FILE 1186 | cluster = self.allocate_cluster() 1187 | length = 1 1188 | else: 1189 | mode |= DF_FILE 1190 | mode &= ~DF_DIR 1191 | cluster = PS2MC_FAT_CHAIN_END 1192 | length = 0 1193 | ent[0] = mode | DF_EXISTS 1194 | ent[1] = 0 1195 | ent[2] = length 1196 | ent[3] = now 1197 | ent[4] = cluster 1198 | ent[5] = 0 1199 | ent[6] = now 1200 | ent[7] = 0 1201 | ent[8] = name[:32] 1202 | dir.write_raw_ent(i, ent, True) 1203 | dir.close() 1204 | 1205 | if mode & DF_FILE: 1206 | # print "@@@ ret", dirloc, ent 1207 | return (dirloc, ent) 1208 | 1209 | dirent = pack_dirent((DF_RWX | DF_0400 | DF_DIR | DF_EXISTS, 1210 | 0, 0, now, dirloc[0], dirloc[1], 1211 | now, 0, ".")) 1212 | dirent += "\0" * (self.cluster_size - PS2MC_DIRENT_LENGTH) 1213 | self.write_allocatable_cluster(cluster, dirent) 1214 | dir = self._directory(dirloc, cluster, 1, "wb", 1215 | name = "") 1216 | dir.write_raw_ent(1, (DF_RWX | DF_0400 | DF_DIR | DF_EXISTS, 1217 | 0, 0, now, 1218 | 0, 0, 1219 | now, 0, ".."), False) 1220 | dir.close() 1221 | ent[2] = 2 1222 | # print "@@@ ret", dirloc, ent 1223 | return (dirloc, ent) 1224 | 1225 | def delete_dirloc(self, dirloc, truncate, name): 1226 | """Delete or truncate the file or directory given by dirloc.""" 1227 | 1228 | if dirloc == (0, 0): 1229 | raise io_error, (EACCES, 1230 | "cannot remove root directory", 1231 | name) 1232 | if dirloc[1] in [0, 1]: 1233 | raise io_error, (EACCES, 1234 | 'cannot remove "." or ".." entries', 1235 | name) 1236 | 1237 | if dirloc in self.open_files: 1238 | raise io_error, (EBUSY, 1239 | "cannot remove open file", filename) 1240 | 1241 | epc = self.entries_per_cluster 1242 | 1243 | ent = self._dirloc_to_ent(dirloc) 1244 | cluster = ent[4] 1245 | if truncate: 1246 | ent[2] = 0 1247 | ent[4] = PS2MC_FAT_CHAIN_END 1248 | ent[6] = tod_now() 1249 | else: 1250 | ent[0] &= ~DF_EXISTS 1251 | self.update_dirent_all(dirloc, None, ent) 1252 | 1253 | while cluster != PS2MC_FAT_CHAIN_END: 1254 | if cluster / epc < self.fat_cursor: 1255 | self.fat_cursor = cluster / epc 1256 | next_cluster = self.lookup_fat(cluster) 1257 | if next_cluster & PS2MC_FAT_ALLOCATED_BIT == 0: 1258 | # corrupted 1259 | break 1260 | next_cluster &= ~PS2MC_FAT_ALLOCATED_BIT 1261 | self.set_fat(cluster, next_cluster) 1262 | if next_cluster == PS2MC_FAT_CHAIN_END_UNALLOC: 1263 | break 1264 | cluster = next_cluster 1265 | 1266 | def path_search(self, pathname): 1267 | """Parse and resolve a pathname. 1268 | 1269 | Return a tuple containing a tuple containing three 1270 | values. The first is either the dirloc of the file or 1271 | directory, if it exists, otherwise it's the dirloc the 1272 | pathname's parent directory, if that exists otherwise 1273 | it's None. The second component is directory entry 1274 | for pathname if it exists, otherwise its dummy entry 1275 | with the first element set to 0, and the last element 1276 | set to the final component of the pathname. The third 1277 | is a boolean value that's true if the pathname refers 1278 | a directory.""" 1279 | 1280 | # print "@@@ path_search", repr(pathname) 1281 | if pathname == "": 1282 | return (None, None, False) 1283 | 1284 | (components, relative, is_dir) = pathname_split(pathname) 1285 | 1286 | dirloc = (0, 0) 1287 | if relative: 1288 | dirloc = self.curdir 1289 | 1290 | tmpname = "" 1291 | _directory = self._directory 1292 | 1293 | if dirloc == (0, 0): 1294 | rootent = self.read_allocatable_cluster(0) 1295 | ent = unpack_dirent(rootent[:PS2MC_DIRENT_LENGTH]) 1296 | dir_cluster = 0 1297 | dir = _directory(dirloc, dir_cluster, ent[2], 1298 | name = tmpname) 1299 | else: 1300 | ent = self._dirloc_to_ent(dirloc) 1301 | dir = _directory(dirloc, ent[4], ent[2], 1302 | name = tmpname) 1303 | 1304 | for s in components: 1305 | # print "@@@", dirloc, repr(s), dir == None, ent 1306 | 1307 | if dir == None: 1308 | # tried to traverse a file or a 1309 | # non-existent directory 1310 | return (None, (0, 0, 0, 0, 0, 0, 0, 0, None), 1311 | False) 1312 | 1313 | if s == ".": 1314 | continue 1315 | if s == "..": 1316 | dotent = dir[0] 1317 | dir.close() 1318 | dirloc = (dotent[4], dotent[5]) 1319 | ent = self._dirloc_to_ent(dirloc) 1320 | dir = _directory(dirloc, ent[4], ent[2], 1321 | name = tmpname) 1322 | continue 1323 | 1324 | dir_cluster = ent[4] 1325 | (i, ent) = self.search_directory(dir, s) 1326 | dir.close() 1327 | dir = None 1328 | 1329 | if ent == None: 1330 | continue 1331 | 1332 | dirloc = (dir_cluster, i) 1333 | if ent[0] & DF_DIR: 1334 | dir = _directory(dirloc, ent[4], ent[2], 1335 | name = tmpname) 1336 | 1337 | if dir != None: 1338 | dir.close() 1339 | is_dir = True 1340 | elif ent != None: 1341 | is_dir = False 1342 | 1343 | if ent == None: 1344 | ent = (0, 0, 0, 0, 0, 0, 0, 0, components[-1]) 1345 | 1346 | return (dirloc, ent, is_dir) 1347 | 1348 | def open(self, filename, mode = "r"): 1349 | """Open a file, returning a new file-like object for it.""" 1350 | 1351 | (dirloc, ent, is_dir) = self.path_search(filename) 1352 | # print "@@@ open", (dirloc, ent) 1353 | if dirloc == None: 1354 | raise path_not_found, filename 1355 | if is_dir: 1356 | raise io_error, (EISDIR, "not a regular file", 1357 | filename) 1358 | if ent[0] == 0: 1359 | if mode[0] not in "wa": 1360 | raise file_not_found, filename 1361 | name = ent[8] 1362 | (dirloc, ent) = self.create_dir_entry(dirloc, name, 1363 | DF_FILE | DF_RWX 1364 | | DF_0400); 1365 | self.flush() 1366 | elif mode[0] == "w": 1367 | self.delete_dirloc(dirloc, True, filename) 1368 | ent[4] = PS2MC_FAT_CHAIN_END 1369 | ent[2] = 0 1370 | return self.file(dirloc, ent[4], ent[2], mode, filename) 1371 | 1372 | def dir_open(self, filename, mode = "rb"): 1373 | (dirloc, ent, is_dir) = self.path_search(filename) 1374 | if dirloc == None: 1375 | raise path_not_found, filename 1376 | if ent[0] == 0: 1377 | raise dir_not_found, filename 1378 | if not is_dir: 1379 | raise io_error, (ENOTDIR, "not a directory", filename) 1380 | return self.directory(dirloc, ent[4], ent[2], mode, filename) 1381 | 1382 | def mkdir(self, filename): 1383 | (dirloc, ent, is_dir) = self.path_search(filename) 1384 | if dirloc == None: 1385 | raise path_not_found, filename 1386 | if ent[0] != 0: 1387 | raise io_error, (EEXIST, "directory exists", filename) 1388 | name = ent[8] 1389 | self.create_dir_entry(dirloc, name, DF_DIR | DF_RWX | DF_0400) 1390 | self.flush() 1391 | 1392 | def _is_empty(self, dirloc, ent, filename): 1393 | """Check if a directory is empty.""" 1394 | 1395 | dir = self._directory(dirloc, ent[4], ent[2], "rb", 1396 | filename) 1397 | try: 1398 | for i in range(2, len(dir)): 1399 | if dir[i][0] & DF_EXISTS: 1400 | return False 1401 | finally: 1402 | dir.close() 1403 | return True 1404 | 1405 | def remove(self, filename): 1406 | """Remove a file or empty directory.""" 1407 | 1408 | (dirloc, ent, is_dir) = self.path_search(filename) 1409 | if dirloc == None: 1410 | raise path_not_found, filename 1411 | if ent[0] == 0: 1412 | raise file_not_found, filename 1413 | if is_dir: 1414 | if ent[4] == 0: 1415 | raise io_error, (EACCES, 1416 | "cannot remove" 1417 | " root directory") 1418 | if not self._is_empty(dirloc, ent, filename): 1419 | raise io_error, (ENOTEMPTY, 1420 | "directory not empty", 1421 | filename) 1422 | self.delete_dirloc(dirloc, False, filename) 1423 | self.flush() 1424 | 1425 | def chdir(self, filename): 1426 | (dirloc, ent, is_dir) = self.path_search(filename) 1427 | if dirloc == None: 1428 | raise path_not_found, filename 1429 | if ent[0] == 0: 1430 | raise dir_not_found, filename 1431 | if not is_dir: 1432 | raise io_error, (ENOTDIR, "not a directory", filename) 1433 | self.curdir = dirloc 1434 | 1435 | def get_mode(self, filename): 1436 | """Get mode bits of a file. 1437 | 1438 | Returns None if the filename doesn't exist, rather than 1439 | throwing a error.""" 1440 | 1441 | (dirloc, ent, is_dir) = self.path_search(filename) 1442 | if ent[0] == 0: 1443 | return None 1444 | return ent[0] 1445 | 1446 | def get_dirent(self, filename): 1447 | """Get the raw directory entry tuple for a file.""" 1448 | 1449 | (dirloc, ent, is_dir) = self.path_search(filename) 1450 | if dirloc == None: 1451 | raise path_not_found, filename 1452 | if ent[0] == 0: 1453 | raise file_not_found, filename 1454 | return ent 1455 | 1456 | def set_dirent(self, filename, new_ent): 1457 | """Set various directory entry fields of a file. 1458 | 1459 | Not all fields can be changed. If a field in new_ent 1460 | is set to None then is not changed.""" 1461 | 1462 | (dirloc, ent, is_dir) = self.path_search(filename) 1463 | if dirloc == None: 1464 | raise path_not_found, filename 1465 | if ent[0] == 0: 1466 | raise file_not_found, filename 1467 | dir = self._opendir_parent_dirloc(dirloc, "r+b") 1468 | try: 1469 | new_ent = list(new_ent) 1470 | new_ent[8] = None 1471 | dir[dirloc[1]] = new_ent 1472 | finally: 1473 | dir.close() 1474 | self.flush() 1475 | return ent 1476 | 1477 | def is_ancestor(self, dirloc, olddirloc): 1478 | while True: 1479 | if dirloc == olddirloc: 1480 | return True 1481 | if dirloc == (0, 0): 1482 | return False 1483 | dirloc = self._get_parent_dirloc(dirloc) 1484 | 1485 | def rename(self, oldpathname, newpathname): 1486 | (olddirloc, oldent, is_dir) = self.path_search(oldpathname) 1487 | if olddirloc == None: 1488 | raise path_not_found, oldpathname 1489 | if oldent[0] == 0: 1490 | raise file_not_found, oldpathname 1491 | 1492 | if olddirloc == (0, 0): 1493 | raise io_error, (EINVAL, 1494 | "cannot rename root directory", 1495 | oldpathname) 1496 | if olddirloc in self.open_files: 1497 | raise io_error, (EBUSY, "cannot rename open file", 1498 | newname) 1499 | 1500 | (newparentdirloc, newent, x) = self.path_search(newpathname) 1501 | if newparentdirloc == None: 1502 | raise path_not_found, newpathname 1503 | if newent[0] != 0: 1504 | raise io_error, (EEXIST, "file exists", newpathname) 1505 | newname = newent[8] 1506 | 1507 | oldparentdirloc = self._get_parent_dirloc(olddirloc) 1508 | if oldparentdirloc == newparentdirloc: 1509 | dir = self._opendir_dirloc(oldparentdirloc, "r+b") 1510 | try: 1511 | dir[olddirloc[1]] = (None, None, None, None, 1512 | None, None, None, None, 1513 | newname) 1514 | finally: 1515 | dir.close() 1516 | return 1517 | 1518 | if is_dir and self.is_ancestor(newparentdirloc, olddirloc): 1519 | raise io_error, (EINVAL, "cannot move directory" 1520 | " beneath itself", oldpathname) 1521 | 1522 | 1523 | newparentdir = None 1524 | newent = None 1525 | try: 1526 | tmpmode = (oldent[0] & ~DF_DIR) | DF_FILE 1527 | 1528 | (newdirloc, newent) \ 1529 | = self.create_dir_entry(newparentdirloc, 1530 | newname, tmpmode) 1531 | 1532 | newent[:8] = oldent[:8] 1533 | newparentdir = self._opendir_dirloc(newparentdirloc) 1534 | newparentdir.write_raw_ent(newdirloc[1], newent, True) 1535 | newent = None 1536 | 1537 | oldent[0] &= ~DF_EXISTS 1538 | self.update_dirent_all(olddirloc, None, oldent) 1539 | 1540 | except: 1541 | if newent != None: 1542 | self.delete_dirloc(newdirloc, False, 1543 | newpathname) 1544 | finally: 1545 | if newparentdir != None: 1546 | newparentdir.close() 1547 | 1548 | if not is_dir: 1549 | return 1550 | 1551 | newdir = self._opendir_dirloc(newdirloc) 1552 | try: 1553 | dotent = list(newdir[0]) 1554 | dotent[4:6] = newdirloc 1555 | newdir.write_raw_ent(0, dotent, False) 1556 | finally: 1557 | newdir.close() 1558 | 1559 | 1560 | def import_save_file(self, sf, ignore_existing, dirname = None): 1561 | """Copy the contents a ps2_save_file object to a directory. 1562 | 1563 | If ingore_existing is true and the directory being imported 1564 | to already exists then False is returned instead of raising 1565 | an error. If dirname is given then the save file is copied 1566 | to that directory instead of the directory specified by 1567 | the save file. 1568 | """ 1569 | 1570 | dir_ent = sf.get_directory() 1571 | if dirname == None: 1572 | dirname = "/" + dir_ent[8] 1573 | 1574 | (root_dirloc, ent, is_dir) = self.path_search(dirname) 1575 | if root_dirloc == None: 1576 | raise path_not_found, dirname 1577 | if ent[0] != 0: 1578 | if ignore_existing: 1579 | return False 1580 | raise io_error, (EEXIST, "directory exists", dirname) 1581 | name = ent[8] 1582 | mode = DF_DIR | (dir_ent[0] & ~DF_FILE) 1583 | 1584 | (dir_dirloc, ent) = self.create_dir_entry(root_dirloc, 1585 | name, mode) 1586 | try: 1587 | assert dirname != "/" 1588 | dirname = dirname + "/" 1589 | for i in range(dir_ent[2]): 1590 | (ent, data) = sf.get_file(i) 1591 | mode = DF_FILE | (ent[0] & ~DF_DIR) 1592 | (dirloc, ent) \ 1593 | = self.create_dir_entry(dir_dirloc, 1594 | ent[8], mode) 1595 | # print "@@@ file", dirloc, ent[4], ent[2] 1596 | f = self.file(dirloc, ent[4], ent[2], "wb", 1597 | dirname + ent[8]) 1598 | try: 1599 | f.write(data) 1600 | finally: 1601 | f.close() 1602 | except EnvironmentError: 1603 | type, what, where = sys.exc_info() 1604 | try: 1605 | try: 1606 | for i in range(dir_ent[2]): 1607 | (ent, data) = sf.get_file(i) 1608 | # print "@@@ remove", ent[8] 1609 | self.remove(dirname + ent[8]) 1610 | except EnvironmentError, why: 1611 | # print "@@@ failed", why 1612 | pass 1613 | 1614 | try: 1615 | # print "@@@ remove dir", dirname 1616 | self.remove(dirname) 1617 | except EnvironmentError, why: 1618 | # print "@@@ failed", why 1619 | pass 1620 | raise type, what, where 1621 | finally: 1622 | del where 1623 | 1624 | # set modes and timestamps to those of the save file 1625 | 1626 | dir = self._opendir_dirloc(dir_dirloc, "r+b") 1627 | try: 1628 | for i in range(dir_ent[2]): 1629 | dir[i + 2] = sf.get_file(i)[0] 1630 | finally: 1631 | dir.close() 1632 | 1633 | dir = self._opendir_dirloc(root_dirloc, "r+b") 1634 | try: 1635 | a = dir_ent[:] 1636 | a[8] = None # don't change the name 1637 | dir[dir_dirloc[1]] = a 1638 | finally: 1639 | dir.close() 1640 | 1641 | self.flush() 1642 | return True 1643 | 1644 | def export_save_file(self, filename): 1645 | (dir_dirloc, dirent, is_dir) = self.path_search(filename) 1646 | if dir_dirloc == None: 1647 | raise path_not_found, filename 1648 | if dirent[0] == 0: 1649 | raise dir_not_found, filename 1650 | if not is_dir: 1651 | raise io_error, (ENOTDIR, "not a directory", filename) 1652 | if dir_dirloc == (0, 0): 1653 | raise io_error, (EACCES, "can't export root directory", 1654 | filename) 1655 | sf = ps2save.ps2_save_file() 1656 | files = [] 1657 | f = None 1658 | dir = self._directory(dir_dirloc, dirent[4], dirent[2], 1659 | "rb", filename) 1660 | try: 1661 | for i in range(2, dirent[2]): 1662 | ent = dir[i] 1663 | if not mode_is_file(ent[0]): 1664 | print ("warning: %s/%s is not a file," 1665 | " ingored." 1666 | % (dirent[8], ent[8])) 1667 | continue 1668 | f = self.file((dirent[4], i), ent[4], ent[2], 1669 | "rb") 1670 | data = f.read(ent[2]) 1671 | f.close() 1672 | assert len(data) == ent[2] 1673 | files.append((ent, data)) 1674 | finally: 1675 | if f != None: 1676 | f.close() 1677 | dir.close() 1678 | dirent[2] = len(files) 1679 | sf.set_directory(dirent) 1680 | for (i, (ent, data)) in enumerate(files): 1681 | sf.set_file(i, ent, data) 1682 | return sf 1683 | 1684 | def _remove_dir(self, dirloc, ent, dirname): 1685 | """Recurse over a directory tree to remove it. 1686 | If not "", dirname must end with a slash (/).""" 1687 | 1688 | first_cluster = ent[4] 1689 | length = ent[2] 1690 | dir = self._directory(dirloc, first_cluster, length, 1691 | "rb", dirname) 1692 | try: 1693 | ents = list(enumerate(dir)) 1694 | finally: 1695 | dir.close() 1696 | for (i, ent) in ents[2:]: 1697 | mode = ent[0] 1698 | if not (mode & DF_EXISTS): 1699 | continue 1700 | if mode & DF_DIR: 1701 | self._remove_dir((first_cluster, i), ent, 1702 | dirname + ent[8] + "/") 1703 | else: 1704 | # print "deleting", dirname + ent[8] 1705 | self.delete_dirloc((first_cluster, i), False, 1706 | dirname + ent[8]) 1707 | self.delete_dirloc(dirloc, False, dirname) 1708 | 1709 | def rmdir(self, dirname): 1710 | """Recursively delete a directory.""" 1711 | 1712 | (dirloc, ent, is_dir) = self.path_search(dirname) 1713 | if dirloc == None: 1714 | raise path_not_found, dirname 1715 | if ent[0] == 0: 1716 | raise dir_not_found, dirname 1717 | if not is_dir: 1718 | raise io_error, (ENOTDIR, "not a directory", dirname) 1719 | if dirloc == (0, 0): 1720 | raise io_error, (EACCES, "can't delete root directory", 1721 | dirname) 1722 | 1723 | if dirname != "" and dirname[-1] != "/": 1724 | dirname += "/" 1725 | self._remove_dir(dirloc, ent, dirname) 1726 | 1727 | def get_free_space(self): 1728 | """Returns the amount of free space in bytes.""" 1729 | 1730 | free = 0 1731 | for i in xrange(self.allocatable_cluster_end): 1732 | if (self.lookup_fat(i) & PS2MC_FAT_ALLOCATED_BIT) == 0: 1733 | free += 1 1734 | return free * self.cluster_size 1735 | 1736 | def get_allocatable_space(self): 1737 | """Returns the total amount of allocatable space in bytes.""" 1738 | return self.allocatable_cluster_limit * self.cluster_size 1739 | 1740 | def _check_file(self, fat, first_cluster, length): 1741 | cluster = first_cluster 1742 | i = 0 1743 | while cluster != PS2MC_FAT_CHAIN_END: 1744 | if cluster < 0 or cluster >= len(fat): 1745 | return "invalid cluster in chain" 1746 | if fat[cluster]: 1747 | return "cross linked chain" 1748 | i += 1 1749 | # print cluster, 1750 | fat[cluster] = 1 1751 | next = self.lookup_fat(cluster) 1752 | if next == PS2MC_FAT_CHAIN_END: 1753 | break 1754 | if (next & PS2MC_FAT_ALLOCATED_BIT) == 0: 1755 | return "unallocated cluster in chain" 1756 | cluster = next & ~PS2MC_FAT_ALLOCATED_BIT 1757 | file_cluster_end = div_round_up(length, self.cluster_size) 1758 | if i < file_cluster_end: 1759 | return "chain ends before end of file" 1760 | elif i > file_cluster_end: 1761 | return "chain continues after end of file" 1762 | return None 1763 | 1764 | def _check_dir(self, fat, dirloc, dirname, ent): 1765 | why = self._check_file(fat, ent[4], 1766 | ent[2] * PS2MC_DIRENT_LENGTH) 1767 | if why != None: 1768 | print "bad directory:", dirname + ":", why 1769 | return False 1770 | ret = True 1771 | first_cluster = ent[4] 1772 | length = ent[2] 1773 | dir = self._directory(dirloc, first_cluster, length, 1774 | "rb", dirname) 1775 | dot_ent = dir[0] 1776 | if dot_ent[8] != ".": 1777 | print "bad directory:", dirname + ': missing "." entry' 1778 | ret = False 1779 | if (dot_ent[4], dot_ent[5]) != dirloc: 1780 | print "bad directory:", dirname + ': bad "." entry' 1781 | ret = False 1782 | if dir[1][8] != "..": 1783 | print "bad directory:", (dirname 1784 | + ': missing ".." entry') 1785 | ret = False 1786 | for i in xrange(2, length): 1787 | ent = dir[i] 1788 | mode = ent[0] 1789 | if not (mode & DF_EXISTS): 1790 | continue 1791 | if mode & DF_DIR: 1792 | if not self._check_dir(fat, (first_cluster, i), 1793 | dirname + ent[8] + "/", 1794 | ent): 1795 | ret = False 1796 | else: 1797 | why = self._check_file(fat, ent[4], ent[2]) 1798 | if why != None: 1799 | print "bad file:", (dirname + ent[8] 1800 | + ":"), why 1801 | ret = False 1802 | 1803 | dir.close() 1804 | return ret 1805 | 1806 | def check(self): 1807 | """Run a simple file system check. 1808 | 1809 | Any problems found are reported to stdout.""" 1810 | 1811 | ret = True 1812 | 1813 | fat_len = int(str(self.allocatable_cluster_end)) 1814 | if not isinstance(fat_len, int): 1815 | raise error, "Memory card image too big to check." 1816 | 1817 | fat = array.array('B', [0]) * fat_len 1818 | 1819 | cluster = self.read_allocatable_cluster(0) 1820 | ent = unpack_dirent(cluster[:PS2MC_DIRENT_LENGTH]) 1821 | ret = self._check_dir(fat, (0, 0), "/", ent) 1822 | 1823 | lost_clusters = 0 1824 | for i in xrange(self.allocatable_cluster_end): 1825 | a = self.lookup_fat(i) 1826 | if (a & PS2MC_FAT_ALLOCATED_BIT) and not fat[i]: 1827 | print i, 1828 | lost_clusters += 1 1829 | if lost_clusters > 0: 1830 | print 1831 | print "found", lost_clusters, "lost clusters" 1832 | ret = False 1833 | 1834 | return ret 1835 | 1836 | def _globdir(self, dirname, components, is_dir): 1837 | pattern = components[0] 1838 | if dirname == "": 1839 | dir = self.dir_open(".") 1840 | else: 1841 | dir = self.dir_open(dirname) 1842 | try: 1843 | return [dirname + ent[8] 1844 | for ent in dir 1845 | if ((ent[0] & DF_EXISTS) 1846 | and (not is_dir or (ent[8] & DF_DIR)) 1847 | and (ent[8] not in [".", ".."] 1848 | or ent[8] == pattern) 1849 | and fnmatch.fnmatchcase(ent[8], 1850 | pattern))] 1851 | finally: 1852 | dir.close() 1853 | 1854 | def _glob(self, dirname, components, is_dir): 1855 | pattern = components[0] 1856 | components = components[1:] 1857 | 1858 | if len(components) == 1: 1859 | _glob = self._globdir 1860 | else: 1861 | _glob = self._glob 1862 | 1863 | if dirname == "": 1864 | dir = self.dir_open(".") 1865 | else: 1866 | dir = self.dir_open(dirname) 1867 | try: 1868 | ret = [] 1869 | for ent in dir: 1870 | name = ent[8] 1871 | if ((ent[0] & DF_EXISTS) == 0 1872 | or (ent[0] & DF_DIR) == 0): 1873 | continue 1874 | if name == "." or name == "..": 1875 | if pattern != name: 1876 | continue 1877 | elif not fnmatch.fnmatchcase(name, pattern): 1878 | continue 1879 | ret += _glob(dirname + name + "/", 1880 | components, is_dir) 1881 | finally: 1882 | dir.close() 1883 | return ret 1884 | 1885 | def glob(self, pattern): 1886 | if pattern == "": 1887 | return [""] 1888 | (components, relative, isdir) = pathname_split(pattern) 1889 | if len(components) == 0: 1890 | return ["/"] 1891 | if relative: 1892 | dirname = "" 1893 | else: 1894 | dirname = "/" 1895 | if len(components) == 1: 1896 | ret = self._globdir(dirname, components, isdir) 1897 | else: 1898 | ret = self._glob(dirname, components, isdir) 1899 | # print pattern, "->", ret 1900 | return ret 1901 | 1902 | def get_icon_sys(self, dirname): 1903 | """Get contents of a directory's icon.sys file, if it exits.""" 1904 | 1905 | icon_sys = dirname + "/icon.sys" 1906 | mode = self.get_mode(icon_sys) 1907 | if mode == None or not mode_is_file(mode): 1908 | return None 1909 | f = self.open(icon_sys, "rb") 1910 | s = f.read(964) 1911 | f.close() 1912 | if len(s) == 964 and s[0:4] == "PS2D": 1913 | return s; 1914 | return None 1915 | 1916 | def dir_size(self, dirname): 1917 | """Calculate the total size of the contents of a directory.""" 1918 | 1919 | dir = self.dir_open(dirname) 1920 | try: 1921 | length = round_up(len(dir) * PS2MC_DIRENT_LENGTH, 1922 | self.cluster_size) 1923 | for ent in dir: 1924 | if mode_is_file(ent[0]): 1925 | length += round_up(ent[2], 1926 | self.cluster_size) 1927 | elif (mode_is_dir(ent[0]) 1928 | and ent[8] not in [".", ".."]): 1929 | length += self.dir_size(dirname + "/" 1930 | + ent[8]) 1931 | finally: 1932 | dir.close() 1933 | return length 1934 | 1935 | def flush(self): 1936 | self.flush_alloc_cluster_cache() 1937 | self.flush_fat_cache() 1938 | if self.modified: 1939 | self.write_superblock() 1940 | self.f.flush() 1941 | 1942 | def close(self): 1943 | """Close all open files. 1944 | 1945 | Disconnects, but doesn't close the file object used 1946 | access the raw image. After this method has been 1947 | called on a ps2mc object, it can no longer be used.""" 1948 | 1949 | # print "ps2mc.close" 1950 | try: 1951 | f = self.f 1952 | if f == None or getattr(f, "closed", False): 1953 | # print "closed" 1954 | return 1955 | open_files = self.open_files 1956 | # print "open_files", open_files 1957 | if open_files != None: 1958 | # this is complicated by the fact as 1959 | # files are closed they will remove 1960 | # themselves from the list of open files 1961 | for (dir, files) in open_files.values(): 1962 | for f in list(files): 1963 | f.close() 1964 | while len(open_files) > 0: 1965 | (k, v) = open_files.popitem() 1966 | (dir, files) = v 1967 | if dir != None: 1968 | dir.close() 1969 | if self.rootdir != None: 1970 | self.rootdir.close() 1971 | if self.fat_cache != None: 1972 | self.flush() 1973 | finally: 1974 | self.open_files = None 1975 | self.fat_cache = None 1976 | self.f = None 1977 | self.rootdir = None 1978 | 1979 | def __del__(self): 1980 | # print "ps2mc.__del__" 1981 | try: 1982 | self.close() 1983 | except: 1984 | sys.stderr.write("ps2mc.__del__: \n") 1985 | traceback.print_exc() 1986 | -------------------------------------------------------------------------------- /ps2mc_dir.py: -------------------------------------------------------------------------------- 1 | # 2 | # ps2mc_dir.py 3 | # 4 | # By Ross Ridge 5 | # Public Domain 6 | # 7 | 8 | """Functions for working with PS2 memory card directory entries.""" 9 | 10 | _SCCS_ID = "@(#) mymc ps2mc_dir.py 1.4 12/10/04 19:11:08\n" 11 | 12 | import struct 13 | import time 14 | import calendar 15 | 16 | PS2MC_DIRENT_LENGTH = 512 17 | 18 | DF_READ = 0x0001 19 | DF_WRITE = 0x0002 20 | DF_EXECUTE = 0x0004 21 | DF_RWX = DF_READ | DF_WRITE | DF_EXECUTE 22 | DF_PROTECTED = 0x0008 23 | DF_FILE = 0x0010 24 | DF_DIR = 0x0020 25 | DF_O_DCREAT = 0x0040 26 | DF_0080 = 0x0080 27 | DF_0100 = 0x0100 28 | DF_O_CREAT = 0x0200 29 | DF_0400 = 0x0400 30 | DF_POCKETSTN = 0x0800 31 | DF_PSX = 0x1000 32 | DF_HIDDEN = 0x2000 33 | DF_4000 = 0x4000 34 | DF_EXISTS = 0x8000 35 | 36 | def zero_terminate(s): 37 | """Truncate a string at the first NUL ('\0') character, if any.""" 38 | 39 | i = s.find('\0') 40 | if i == -1: 41 | return s 42 | return s[:i] 43 | 44 | # mode, ???, length, created, 45 | # fat_cluster, parent_entry, modified, attr, 46 | # name 47 | _dirent_fmt = "> 1)) 42 | a = (a ^ (a >> 2)) 43 | a = (a ^ (a >> 4)) 44 | return a & 1 45 | 46 | def _make_ecc_tables(): 47 | parity_table = [_parityb(b) 48 | for b in range(256)] 49 | cpmasks = [0x55, 0x33, 0x0F, 0x00, 0xAA, 0xCC, 0xF0] 50 | 51 | column_parity_masks = [None] * 256 52 | for b in range(256): 53 | mask = 0 54 | for i in range(len(cpmasks)): 55 | mask |= parity_table[b & cpmasks[i]] << i 56 | column_parity_masks[b] = mask 57 | 58 | return parity_table, column_parity_masks 59 | 60 | _parity_table, _column_parity_masks = _make_ecc_tables() 61 | 62 | def _ecc_calculate(s): 63 | "Calculate the Hamming code for a 128 byte long string or byte array." 64 | 65 | if not isinstance(s, array.array): 66 | a = array.array('B') 67 | a.fromstring(s) 68 | s = a 69 | column_parity = 0x77 70 | line_parity_0 = 0x7F 71 | line_parity_1 = 0x7F 72 | for i in range(len(s)): 73 | b = s[i] 74 | column_parity ^= _column_parity_masks[b] 75 | if _parity_table[b]: 76 | line_parity_0 ^= ~i 77 | line_parity_1 ^= i 78 | return [column_parity, line_parity_0 & 0x7F, line_parity_1] 79 | 80 | def _ecc_check(s, ecc): 81 | """Detect and correct any single bit errors. 82 | 83 | The parameters "s" and "ecc", the data and expected Hamming code 84 | repectively, must be modifiable sequences of integers and are 85 | updated with the corrected values if necessary.""" 86 | 87 | computed = ecc_calculate(s) 88 | if computed == ecc: 89 | return ECC_CHECK_OK 90 | 91 | #print 92 | #_print_bin(0, s.tostring()) 93 | #print "computed %02x %02x %02x" % tuple(computed) 94 | #print "actual %02x %02x %02x" % tuple(ecc) 95 | 96 | # ECC mismatch 97 | 98 | cp_diff = (computed[0] ^ ecc[0]) & 0x77 99 | lp0_diff = (computed[1] ^ ecc[1]) & 0x7F 100 | lp1_diff = (computed[2] ^ ecc[2]) & 0x7F 101 | lp_comp = lp0_diff ^ lp1_diff 102 | cp_comp = (cp_diff >> 4) ^ (cp_diff & 0x07) 103 | 104 | #print "%02x %02x %02x %02x %02x" % (cp_diff, lp0_diff, lp1_diff, 105 | # lp_comp, cp_comp) 106 | 107 | if lp_comp == 0x7F and cp_comp == 0x07: 108 | print "corrected 1" 109 | # correctable 1 bit error in data 110 | s[lp1_diff] ^= 1 << (cp_diff >> 4) 111 | return ECC_CHECK_CORRECTED 112 | if ((cp_diff == 0 and lp0_diff == 0 and lp1_diff == 0) 113 | or _popcount(lp_comp) + _popcount(cp_comp) == 1): 114 | print "corrected 2" 115 | # correctable 1 bit error in ECC 116 | # (and/or one of the unused bits was set) 117 | ecc[0] = computed[0] 118 | ecc[1] = computed[1] 119 | ecc[2] = computed[2] 120 | return ECC_CHECK_CORRECTED 121 | 122 | # uncorrectable error 123 | return ECC_CHECK_FAILED 124 | 125 | def ecc_calculate_page(page): 126 | """Return a list of the ECC codes for a PS2 memory card page.""" 127 | return [ecc_calculate(page[i * 128 : i * 128 + 128]) 128 | for i in range(div_round_up(len(page), 128))] 129 | 130 | def ecc_check_page(page, spare): 131 | "Check and correct any single bit errors in a PS2 memory card page." 132 | 133 | failed = False 134 | corrected = False 135 | 136 | #chunks = [(array.array('B', page[i * 128 : i * 128 + 128]), 137 | # map(ord, spare[i * 3 : i * 3 + 3])) 138 | # for i in range(div_round_up(len(page), 128))] 139 | 140 | chunks = [] 141 | for i in range(div_round_up(len(page), 128)): 142 | a = array.array('B') 143 | a.fromstring(page[i * 128 : i * 128 + 128]) 144 | chunks.append((a, map(ord, spare[i * 3 : i * 3 + 3]))) 145 | 146 | r = [ecc_check(s, ecc) 147 | for (s, ecc) in chunks] 148 | ret = ECC_CHECK_OK 149 | if ECC_CHECK_CORRECTED in r: 150 | # rebuild sector and spare from the corrected versions 151 | page = "".join([a[0].tostring() 152 | for a in chunks]) 153 | spare = "".join([chr(a[1][i]) 154 | for a in chunks 155 | for i in range(3)]) 156 | ret = ECC_CHECK_CORRECTED 157 | if ECC_CHECK_FAILED in r: 158 | ret = ECC_CHECK_FAILED 159 | return (ret, page, spare) 160 | 161 | if mymcsup == None: 162 | ecc_calculate = _ecc_calculate 163 | ecc_check = _ecc_check 164 | else: 165 | # _c_ubyte_p = ctypes.POINTER(ctypes.c_ubyte) 166 | def ecc_calculate(s): 167 | aecc = array.array('B', "\0\0\0") 168 | cecc = ctypes.c_ubyte.from_address(aecc.buffer_info()[0]) 169 | mymcsup.ecc_calculate(s, len(s), cecc) 170 | return list(aecc) 171 | 172 | def ecc_check(s, ecc): 173 | cs = ctypes.c_ubyte.from_address(s.buffer_info()[0]) 174 | # print "%08X" % s.buffer_info()[0] 175 | aecc = array.array('B', ecc) 176 | cecc = ctypes.c_ubyte.from_address(aecc.buffer_info()[0]) 177 | ret = mymcsup.ecc_check(cs, len(s), cecc) 178 | ecc[0] = aecc[0] 179 | ecc[1] = aecc[1] 180 | ecc[2] = aecc[2] 181 | return ret 182 | 183 | -------------------------------------------------------------------------------- /ps2save.py: -------------------------------------------------------------------------------- 1 | # 2 | # ps2save.py 3 | # 4 | # By Ross Ridge 5 | # Public Domain 6 | # 7 | # A simple interface for working with various PS2 save file formats. 8 | # 9 | 10 | _SCCS_ID = "@(#) mymc ps2save.py 1.8 22/01/15 01:25:25\n" 11 | 12 | import sys 13 | import os 14 | import string 15 | import struct 16 | import binascii 17 | import array 18 | import zlib 19 | 20 | from round import div_round_up, round_up 21 | from ps2mc_dir import * 22 | from sjistab import shift_jis_normalize_table 23 | 24 | try: 25 | import lzari 26 | except ImportError: 27 | lzari = None 28 | 29 | PS2SAVE_MAX_MAGIC = "Ps2PowerSave" 30 | PS2SAVE_SPS_MAGIC = "\x0d\0\0\0SharkPortSave" 31 | PS2SAVE_CBS_MAGIC = "CFU\0" 32 | PS2SAVE_NPO_MAGIC = "nPort" 33 | 34 | # This is the initial permutation state ("S") for the RC4 stream cipher 35 | # algorithm used to encrpyt and decrypt Codebreaker saves. 36 | PS2SAVE_CBS_RC4S = [0x5f, 0x1f, 0x85, 0x6f, 0x31, 0xaa, 0x3b, 0x18, 37 | 0x21, 0xb9, 0xce, 0x1c, 0x07, 0x4c, 0x9c, 0xb4, 38 | 0x81, 0xb8, 0xef, 0x98, 0x59, 0xae, 0xf9, 0x26, 39 | 0xe3, 0x80, 0xa3, 0x29, 0x2d, 0x73, 0x51, 0x62, 40 | 0x7c, 0x64, 0x46, 0xf4, 0x34, 0x1a, 0xf6, 0xe1, 41 | 0xba, 0x3a, 0x0d, 0x82, 0x79, 0x0a, 0x5c, 0x16, 42 | 0x71, 0x49, 0x8e, 0xac, 0x8c, 0x9f, 0x35, 0x19, 43 | 0x45, 0x94, 0x3f, 0x56, 0x0c, 0x91, 0x00, 0x0b, 44 | 0xd7, 0xb0, 0xdd, 0x39, 0x66, 0xa1, 0x76, 0x52, 45 | 0x13, 0x57, 0xf3, 0xbb, 0x4e, 0xe5, 0xdc, 0xf0, 46 | 0x65, 0x84, 0xb2, 0xd6, 0xdf, 0x15, 0x3c, 0x63, 47 | 0x1d, 0x89, 0x14, 0xbd, 0xd2, 0x36, 0xfe, 0xb1, 48 | 0xca, 0x8b, 0xa4, 0xc6, 0x9e, 0x67, 0x47, 0x37, 49 | 0x42, 0x6d, 0x6a, 0x03, 0x92, 0x70, 0x05, 0x7d, 50 | 0x96, 0x2f, 0x40, 0x90, 0xc4, 0xf1, 0x3e, 0x3d, 51 | 0x01, 0xf7, 0x68, 0x1e, 0xc3, 0xfc, 0x72, 0xb5, 52 | 0x54, 0xcf, 0xe7, 0x41, 0xe4, 0x4d, 0x83, 0x55, 53 | 0x12, 0x22, 0x09, 0x78, 0xfa, 0xde, 0xa7, 0x06, 54 | 0x08, 0x23, 0xbf, 0x0f, 0xcc, 0xc1, 0x97, 0x61, 55 | 0xc5, 0x4a, 0xe6, 0xa0, 0x11, 0xc2, 0xea, 0x74, 56 | 0x02, 0x87, 0xd5, 0xd1, 0x9d, 0xb7, 0x7e, 0x38, 57 | 0x60, 0x53, 0x95, 0x8d, 0x25, 0x77, 0x10, 0x5e, 58 | 0x9b, 0x7f, 0xd8, 0x6e, 0xda, 0xa2, 0x2e, 0x20, 59 | 0x4f, 0xcd, 0x8f, 0xcb, 0xbe, 0x5a, 0xe0, 0xed, 60 | 0x2c, 0x9a, 0xd4, 0xe2, 0xaf, 0xd0, 0xa9, 0xe8, 61 | 0xad, 0x7a, 0xbc, 0xa8, 0xf2, 0xee, 0xeb, 0xf5, 62 | 0xa6, 0x99, 0x28, 0x24, 0x6c, 0x2b, 0x75, 0x5d, 63 | 0xf8, 0xd3, 0x86, 0x17, 0xfb, 0xc0, 0x7b, 0xb3, 64 | 0x58, 0xdb, 0xc7, 0x4b, 0xff, 0x04, 0x50, 0xe9, 65 | 0x88, 0x69, 0xc9, 0x2a, 0xab, 0xfd, 0x5b, 0x1b, 66 | 0x8a, 0xd9, 0xec, 0x27, 0x44, 0x0e, 0x33, 0xc8, 67 | 0x6b, 0x93, 0x32, 0x48, 0xb6, 0x30, 0x43, 0xa5] 68 | 69 | class error(Exception): 70 | """Base for all exceptions specific to this module.""" 71 | pass 72 | 73 | class corrupt(error): 74 | """Corrupt save file.""" 75 | 76 | def __init__(self, msg, f = None): 77 | fn = None 78 | if f != None: 79 | fn = getattr(f, "name", None) 80 | self.filename = fn 81 | error.__init__(self, "Corrupt save file: " + msg) 82 | 83 | class eof(corrupt): 84 | """Save file is truncated.""" 85 | 86 | def __init__(self, f = None): 87 | corrupt.__init__(self, "Unexpected EOF", f) 88 | 89 | class subdir(corrupt): 90 | def __init__(self, f = None): 91 | corrupt.__init__(self, "Non-file in save file.", f) 92 | 93 | # 94 | # Table of graphically similar ASCII characters that can be used 95 | # as substitutes for Unicode characters. 96 | # 97 | char_substs = { 98 | u'\u00a2': u"c", 99 | u'\u00b4': u"'", 100 | u'\u00d7': u"x", 101 | u'\u00f7': u"/", 102 | u'\u2010': u"-", 103 | u'\u2015': u"-", 104 | u'\u2018': u"'", 105 | u'\u2019': u"'", 106 | u'\u201c': u'"', 107 | u'\u201d': u'"', 108 | u'\u2032': u"'", 109 | u'\u2212': u"-", 110 | u'\u226a': u"<<", 111 | u'\u226b': u">>", 112 | u'\u2500': u"-", 113 | u'\u2501': u"-", 114 | u'\u2502': u"|", 115 | u'\u2503': u"|", 116 | u'\u250c': u"+", 117 | u'\u250f': u"+", 118 | u'\u2510': u"+", 119 | u'\u2513': u"+", 120 | u'\u2514': u"+", 121 | u'\u2517': u"+", 122 | u'\u2518': u"+", 123 | u'\u251b': u"+", 124 | u'\u251c': u"+", 125 | u'\u251d': u"+", 126 | u'\u2520': u"+", 127 | u'\u2523': u"+", 128 | u'\u2524': u"+", 129 | u'\u2525': u"+", 130 | u'\u2528': u"+", 131 | u'\u252b': u"+", 132 | u'\u252c': u"+", 133 | u'\u252f': u"+", 134 | u'\u2530': u"+", 135 | u'\u2533': u"+", 136 | u'\u2537': u"+", 137 | u'\u2538': u"+", 138 | u'\u253b': u"+", 139 | u'\u253c': u"+", 140 | u'\u253f': u"+", 141 | u'\u2542': u"+", 142 | u'\u254b': u"+", 143 | u'\u25a0': u"#", 144 | u'\u25a1': u"#", 145 | u'\u2605': u"*", 146 | u'\u2606': u"*", 147 | u'\u3001': u",", 148 | u'\u3002': u".", 149 | u'\u3003': u'"', 150 | u'\u3007': u'0', 151 | u'\u3008': u'<', 152 | u'\u3009': u'>', 153 | u'\u300a': u'<<', 154 | u'\u300b': u'>>', 155 | u'\u300a': u'<<', 156 | u'\u300b': u'>>', 157 | u'\u300c': u'[', 158 | u'\u300d': u']', 159 | u'\u300e': u'[', 160 | u'\u300f': u']', 161 | u'\u3010': u'[', 162 | u'\u3011': u']', 163 | u'\u3014': u'[', 164 | u'\u3015': u']', 165 | u'\u301c': u'~', 166 | u'\u30fc': u'-', 167 | } 168 | 169 | def shift_jis_conv(src, encoding = None): 170 | """Convert Shift-JIS strings to a graphically similar representation. 171 | 172 | If encoding is "unicode" then a Unicode string is returned, otherwise 173 | a string in the encoding specified is returned. If necessary, 174 | graphically similar characters are used to replace characters not 175 | exactly representable in the desired encoding. 176 | """ 177 | 178 | if encoding == None: 179 | encoding = sys.getdefaultencoding() 180 | if encoding == "shift_jis": 181 | return src 182 | u = src.decode("shift_jis", "replace") 183 | if encoding == "unicode": 184 | return u 185 | a = [] 186 | for uc in u: 187 | try: 188 | uc.encode(encoding) 189 | a.append(uc) 190 | except UnicodeError: 191 | for uc2 in shift_jis_normalize_table.get(uc, uc): 192 | a.append(char_substs.get(uc2, uc2)) 193 | 194 | return u"".join(a).encode(encoding, "replace") 195 | 196 | def rc4_crypt(s, t): 197 | """RC4 encrypt/decrypt the string t using the permutation s. 198 | 199 | Returns a byte array.""" 200 | 201 | s = array.array('B', s) 202 | t = array.array('B', t) 203 | j = 0 204 | for ii in range(len(t)): 205 | i = (ii + 1) % 256 206 | j = (j + s[i]) % 256 207 | (s[i], s[j]) = (s[j], s[i]) 208 | t[ii] ^= s[(s[i] + s[j]) % 256] 209 | return t 210 | 211 | # def sps_check(s): 212 | # """Calculate the checksum for a SharkPort save.""" 213 | # 214 | # h = 0 215 | # for c in array.array('B', s): 216 | # h += c << (h % 24) 217 | # h &= 0xFFFFFFFF 218 | # return h 219 | 220 | def unpack_icon_sys(s): 221 | """Unpack an icon.sys file into a tuple.""" 222 | 223 | # magic, title offset, ... 224 | # [14] title, normal icon, copy icon, del icon 225 | a = struct.unpack("<4s2xH4x" 226 | "L" "16s16s16s16s" "16s16s16s" "16s16s16s" "16s" 227 | "68s64s64s64s512x", s) 228 | a = list(a) 229 | for i in range(3, 7): 230 | a[i] = struct.unpack("<4L", a[i]) 231 | a[i] = map(hex, a[i]) 232 | for i in range(7, 14): 233 | a[i] = struct.unpack("<4f", a[i]) 234 | a[14] = zero_terminate(a[14]) 235 | a[15] = zero_terminate(a[15]) 236 | a[16] = zero_terminate(a[16]) 237 | a[17] = zero_terminate(a[17]) 238 | return a 239 | 240 | def icon_sys_title(icon_sys, encoding = None): 241 | """Extract the two lines of the title stored in an icon.sys tuple.""" 242 | 243 | offset = icon_sys[1] 244 | title = icon_sys[14] 245 | title2 = shift_jis_conv(title[offset:], encoding) 246 | title1 = shift_jis_conv(title[:offset], encoding) 247 | return (title1, title2) 248 | 249 | def _read_fixed(f, n): 250 | """Read a string of a fixed length from a file.""" 251 | 252 | s = f.read(n) 253 | if len(s) != n: 254 | raise eof, f 255 | return s 256 | 257 | def _read_long_string(f): 258 | """Read a string prefixed with a 32-bit length from a file.""" 259 | 260 | length = struct.unpack("= 964: 302 | return unpack_icon_sys(data[:964]) 303 | return None 304 | 305 | def load_ems(self, f): 306 | """Load EMS (.psu) save files.""" 307 | 308 | cluster_size = 1024 309 | 310 | dirent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH)) 311 | dotent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH)) 312 | dotdotent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH)) 313 | if (not mode_is_dir(dirent[0]) 314 | or not mode_is_dir(dotent[0]) 315 | or not mode_is_dir(dotdotent[0]) 316 | or dirent[2] < 2): 317 | raise corrupt, ("Not a EMS (.psu) save file.", f) 318 | 319 | dirent[2] -= 2 320 | self.set_directory(dirent) 321 | 322 | for i in range(dirent[2]): 323 | ent = unpack_dirent(_read_fixed(f, 324 | PS2MC_DIRENT_LENGTH)) 325 | if not mode_is_file(ent[0]): 326 | raise subdir, f 327 | flen = ent[2] 328 | self.set_file(i, ent, _read_fixed(f, flen)) 329 | _read_fixed(f, round_up(flen, cluster_size) - flen) 330 | 331 | 332 | def save_ems(self, f): 333 | cluster_size = 1024 334 | 335 | dirent = self.dirent[:] 336 | dirent[2] += 2 337 | f.write(pack_dirent(dirent)) 338 | f.write(pack_dirent((DF_RWX | DF_DIR | DF_0400 | DF_EXISTS, 339 | 0, 0, dirent[3], 340 | 0, 0, dirent[3], 0, "."))) 341 | f.write(pack_dirent((DF_RWX | DF_DIR | DF_0400 | DF_EXISTS, 342 | 0, 0, dirent[3], 343 | 0, 0, dirent[3], 0, ".."))) 344 | 345 | for i in range(dirent[2] - 2): 346 | (ent, data) = self.get_file(i) 347 | f.write(pack_dirent(ent)) 348 | if not mode_is_file(ent[0]): 349 | # print ent 350 | # print hex(ent[0]) 351 | raise error, "Directory has a subdirectory." 352 | f.write(data) 353 | f.write("\0" * (round_up(len(data), cluster_size) 354 | - len(data))) 355 | f.flush() 356 | 357 | def _load_max_drive_2(self): 358 | (length, s) = self._compressed 359 | self._compressed = None 360 | 361 | if lzari == None: 362 | raise error, ("The lzari module is needed to " 363 | " decompress MAX Drive saves.") 364 | s = lzari.decode(s, length, 365 | "decompressing " + self.dirent[8] + ": ") 366 | dirlen = self.dirent[2] 367 | timestamp = self.dirent[3] 368 | off = 0 369 | for i in range(dirlen): 370 | if len(s) - off < 36: 371 | raise eof, f 372 | (l, name) = struct.unpack(" 0 and title[0][-1] != ' ': 419 | iconsysname = title[0] + " " + title[1].strip() 420 | else: 421 | iconsysname = title[0] + title[1].rstrip() 422 | s = "" 423 | dirent = self.dirent 424 | for i in range(dirent[2]): 425 | (ent, data) = self.get_file(i) 426 | if not mode_is_file(ent[0]): 427 | raise error, "Non-file in save file." 428 | s += struct.pack("= 2 582 | and dotent[8] == "." and dotdotent[8] == ".."): 583 | return "psu" 584 | return None 585 | 586 | # 587 | # Set up tables of illegal and problematic characters in file names. 588 | # 589 | _bad_filename_chars = ("".join(map(chr, range(32))) 590 | + "".join(map(chr, range(127, 256)))) 591 | _bad_filename_repl = "_" * len(_bad_filename_chars) 592 | 593 | if os.name in ["nt", "os2", "ce"]: 594 | _bad_filename_chars += '<>:"/\\|?*' 595 | _bad_filename_repl += "()_'_____" 596 | _bad_filename_chars2 = _bad_filename_chars + " " 597 | _bad_filename_repl2 = _bad_filename_repl + "_" 598 | else: 599 | _bad_filename_chars += "/" 600 | _bad_filename_repl += "_" 601 | _bad_filename_chars2 = _bad_filename_chars + "?*'&|:[<>] \\\"" 602 | _bad_filename_repl2 = _bad_filename_repl + "______(())___" 603 | 604 | _filename_trans = string.maketrans(_bad_filename_chars, _bad_filename_repl); 605 | _filename_trans2 = string.maketrans(_bad_filename_chars2, _bad_filename_repl2); 606 | 607 | def fix_filename(filename): 608 | """Replace illegal or problematic characters from a filename.""" 609 | return filename.translate(_filename_trans) 610 | 611 | def make_longname(dirname, sf): 612 | """Return a string containing a verbose filename for a save file.""" 613 | 614 | icon_sys = sf.get_icon_sys() 615 | title = "" 616 | if icon_sys != None: 617 | title = icon_sys_title(icon_sys, "ascii") 618 | title = title[0] + " " + title[1] 619 | title = " ".join(title.split()) 620 | crc = binascii.crc32("") 621 | for (ent, data) in sf: 622 | crc = binascii.crc32(data, crc) 623 | if len(dirname) >= 12 and (dirname[0:2] in ("BA", "BJ", "BE", "BK")): 624 | if dirname[2:6] == "DATA": 625 | title = "" 626 | else: 627 | #dirname = dirname[2:6] + dirname[7:12] 628 | dirname = dirname[2:12] 629 | 630 | return fix_filename("%s %s (%08X)" 631 | % (dirname, title, crc & 0xFFFFFFFF)) 632 | 633 | -------------------------------------------------------------------------------- /round.py: -------------------------------------------------------------------------------- 1 | # 2 | # round.py 3 | # 4 | # By Ross Ridge 5 | # Public Domain 6 | # 7 | # Simple rounding functions. 8 | # 9 | 10 | _SCCS_ID = "@(#) mymc round.py 1.3 07/04/17 02:10:27\n" 11 | 12 | def div_round_up(a, b): 13 | return (a + b - 1) / b 14 | 15 | def round_up(a, b): 16 | return (a + b - 1) / b * b 17 | 18 | def round_down(a, b): 19 | return a / b * b 20 | 21 | 22 | -------------------------------------------------------------------------------- /sjistab.py: -------------------------------------------------------------------------------- 1 | # automatically generated 2 | shift_jis_normalize_table = {u'\uff81': u'\u30c1', u'\u3000': u' ', u'\uff85': u'\u30ca', u'\uff06': u'&', u'\uff89': u'\u30ce', u'\uff0a': u'*', u'\uff8d': u'\u30d8', u'\uff0e': u'.', u'\uff91': u'\u30e0', u'\uff12': u'2', u'\uff95': u'\u30e6', u'\uff16': u'6', u'\uff99': u'\u30eb', u'\u309b': u' \u3099', u'\uff1a': u':', u'\uff9d': u'\u30f3', u'\uff03': u'#', u'\uff1e': u'>', u'\uff22': u'B', u'\uff26': u'F', u'\uff2a': u'J', u'\u222c': u'\u222b\u222b', u'\uff2e': u'N', u'\uff32': u'R', u'\uff36': u'V', u'\uff3a': u'Z', u'\uff3e': u'^', u'\uff42': u'b', u'\uff46': u'f', u'\uff4a': u'j', u'\uff4e': u'n', u'\uff52': u'r', u'\uff56': u'v', u'\uff5a': u'z', u'\uff62': u'\u300c', u'\uffe5': u'\xa5', u'\uff66': u'\u30f2', u'\uff6a': u'\u30a7', u'\uff6e': u'\u30e7', u'\uff72': u'\u30a4', u'\uff76': u'\u30ab', u'\uff7a': u'\u30b3', u'\uff7e': u'\u30bb', u'\uff01': u'!', u'\uff82': u'\u30c4', u'\uff05': u'%', u'\uff86': u'\u30cb', u'\uff09': u')', u'\uff8a': u'\u30cf', u'\uff8e': u'\u30db', u'\uff11': u'1', u'\uff92': u'\u30e1', u'\uff15': u'5', u'\uff96': u'\u30e8', u'\uff19': u'9', u'\uff9a': u'\u30ec', u'\uff1d': u'=', u'\u309c': u' \u309a', u'\uff9e': u'\u3099', u'\uff21': u'A', u'\uff25': u'E', u'\uff29': u'I', u'\xa8': u' \u0308', u'\uff2d': u'M', u'\uff31': u'Q', u'\u2033': u'\u2032\u2032', u'\uff35': u'U', u'\xb4': u' \u0301', u'\uff39': u'Y', u'\uff3d': u']', u'\uff41': u'a', u'\uff45': u'e', u'\uff49': u'i', u'\uff4d': u'm', u'\uff51': u'q', u'\uff55': u'u', u'\uff59': u'y', u'\uff5d': u'}', u'\uff61': u'\u3002', u'\uff65': u'\u30fb', u'\uff69': u'\u30a5', u'\uff6d': u'\u30e5', u'\uff71': u'\u30a2', u'\uff75': u'\u30aa', u'\uff79': u'\u30b1', u'\uff7d': u'\u30b9', u'\uff83': u'\u30c6', u'\uff04': u'$', u'\uff87': u'\u30cc', u'\uff08': u'(', u'\uff8b': u'\u30d2', u'\uff0c': u',', u'\uff8f': u'\u30de', u'\uff10': u'0', u'\uff93': u'\u30e2', u'\uff14': u'4', u'\uff97': u'\u30e9', u'\uff18': u'8', u'\uff9b': u'\u30ed', u'\uff1c': u'<', u'\uff9f': u'\u309a', u'\uff20': u'@', u'\uff24': u'D', u'\u2026': u'...', u'\uff28': u'H', u'\uff2c': u'L', u'\uff30': u'P', u'\uff34': u'T', u'\uff38': u'X', u'\uff3c': u'\\', u'\uff40': u'`', u'\uff44': u'd', u'\uff48': u'h', u'\uff4c': u'l', u'\uff50': u'p', u'\uff54': u't', u'\uff58': u'x', u'\uff5c': u'|', u'\uffe3': u' \u0304', u'\uff64': u'\u3001', u'\uff68': u'\u30a3', u'\uff6c': u'\u30e3', u'\uff70': u'\u30fc', u'\uff74': u'\u30a8', u'\uff78': u'\u30af', u'\uff7c': u'\u30b7', u'\uff80': u'\u30bf', u'\u2103': u'\xb0C', u'\uff84': u'\u30c8', u'\uff88': u'\u30cd', u'\uff0b': u'+', u'\uff8c': u'\u30d5', u'\uff0f': u'/', u'\uff90': u'\u30df', u'\uff13': u'3', u'\uff94': u'\u30e4', u'\uff17': u'7', u'\uff98': u'\u30ea', u'\uff1b': u';', u'\uff9c': u'\u30ef', u'\uff1f': u'?', u'\uff23': u'C', u'\u2025': u'..', u'\uff27': u'G', u'\u212b': u'\xc5', u'\uff2f': u'O', u'\uff33': u'S', u'\uff37': u'W', u'\uff3b': u'[', u'\uff3f': u'_', u'\uff43': u'c', u'\uff47': u'g', u'\uff4b': u'k', u'\uff4f': u'o', u'\uff53': u's', u'\uff57': u'w', u'\uff5b': u'{', u'\uff63': u'\u300d', u'\uff67': u'\u30a1', u'\uff6b': u'\u30a9', u'\uff6f': u'\u30c3', u'\uff73': u'\u30a6', u'\uff77': u'\u30ad', u'\uff7b': u'\u30b5', u'\uff2b': u'K', u'\uff7f': u'\u30bd'} 3 | -------------------------------------------------------------------------------- /verbuild.py: -------------------------------------------------------------------------------- 1 | MYMC_VERSION_BUILD = r'''7''' 2 | MYMC_VERSION_MAJOR = r'''2''' 3 | --------------------------------------------------------------------------------