Home | Trees | Indices | Help |
|
---|
|
1 """ 2 :mod:`pyffi.formats.bsa` --- Bethesda Archive (.bsa) 3 ==================================================== 4 5 .. warning:: 6 7 This module is still a work in progress, 8 and is not yet ready for production use. 9 10 A .bsa file is an archive format used by Bethesda (Morrowind, Oblivion, 11 Fallout 3). 12 13 Implementation 14 -------------- 15 16 .. autoclass:: BsaFormat 17 :show-inheritance: 18 :members: 19 20 Regression tests 21 ---------------- 22 23 Read a BSA file 24 ^^^^^^^^^^^^^^^ 25 26 >>> # check and read bsa file 27 >>> stream = open('tests/bsa/test.bsa', 'rb') 28 >>> data = BsaFormat.Data() 29 >>> data.inspect_quick(stream) 30 >>> data.version 31 103 32 >>> data.inspect(stream) 33 >>> data.folders_offset 34 36 35 >>> hex(data.archive_flags.to_int(data)) 36 '0x703' 37 >>> data.num_folders 38 1 39 >>> data.num_files 40 7 41 >>> #data.read(stream) 42 >>> # TODO check something else... 43 44 Parse all BSA files in a directory tree 45 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 46 47 >>> for stream, data in BsaFormat.walkData('tests/bsa'): 48 ... print(stream.name) 49 tests/bsa/test.bsa 50 51 Create an BSA file from scratch and write to file 52 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 53 54 >>> data = BsaFormat.Data() 55 >>> # TODO store something... 56 >>> from tempfile import TemporaryFile 57 >>> stream = TemporaryFile() 58 >>> #data.write(stream) 59 """ 60 61 # ***** BEGIN LICENSE BLOCK ***** 62 # 63 # Copyright (c) 2007-2011, Python File Format Interface 64 # All rights reserved. 65 # 66 # Redistribution and use in source and binary forms, with or without 67 # modification, are permitted provided that the following conditions 68 # are met: 69 # 70 # * Redistributions of source code must retain the above copyright 71 # notice, this list of conditions and the following disclaimer. 72 # 73 # * Redistributions in binary form must reproduce the above 74 # copyright notice, this list of conditions and the following 75 # disclaimer in the documentation and/or other materials provided 76 # with the distribution. 77 # 78 # * Neither the name of the Python File Format Interface 79 # project nor the names of its contributors may be used to endorse 80 # or promote products derived from this software without specific 81 # prior written permission. 82 # 83 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 84 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 85 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 86 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 87 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 88 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 89 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 90 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 91 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 92 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 93 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 94 # POSSIBILITY OF SUCH DAMAGE. 95 # 96 # ***** END LICENSE BLOCK ***** 97 98 from itertools import izip 99 import logging 100 import struct 101 import os 102 import re 103 104 import pyffi.object_models.xml 105 import pyffi.object_models.common 106 from pyffi.object_models.xml.basic import BasicBase 107 import pyffi.object_models 108 from pyffi.utils.graph import EdgeFilter112 """This class implements the BSA format.""" 113 xml_file_name = 'bsa.xml' 114 # where to look for bsa.xml and in what order: 115 # BSAXMLPATH env var, or BsaFormat module directory 116 xml_file_path = [os.getenv('BSAXMLPATH'), os.path.dirname(__file__)] 117 # file name regular expression match 118 RE_FILENAME = re.compile(r'^.*\.bsa$', re.IGNORECASE) 119 120 # basic types 121 UInt32 = pyffi.object_models.common.UInt 122 ZString = pyffi.object_models.common.ZString 123 124 # implementation of bsa-specific basic types 125 133316 317 if __name__ == '__main__': 318 import doctest 319 doctest.testmod() 320135147137 return 2 + len(self._value)138140 length, = struct.unpack('<B', stream.read(1)) 141 self._value = stream.read(length)[:-1] # strip trailing null byte142149 """Basic type which implements the header of a BSA file.""" 150 153192 193 @staticmethod155 """Read header string from stream and check it. 156 157 :param stream: The stream to read from. 158 :type stream: file 159 """ 160 hdrstr = stream.read(4) 161 # check if the string is correct 162 if hdrstr == "\x00\x01\x00\x00".encode("ascii"): 163 # morrowind style, set version too! 164 self._value = 0 165 elif hdrstr == "BSA\x00".encode("ascii"): 166 # oblivion an up: read version 167 self._value, = struct.unpack("<I", stream.read(4)) 168 else: 169 raise ValueError( 170 "invalid BSA header:" 171 " expected '\\x00\\x01\\x00\\x00' or 'BSA\\x00'" 172 " but got '%s'" % hdrstr)173175 """Write the header string to stream. 176 177 :param stream: The stream to write to. 178 :type stream: file 179 """ 180 if self._value >= 103: 181 stream.write("BSA\x00".encode("ascii")) 182 stream.write(struct.pack("<I", self._value)) 183 else: 184 stream.write("\x00\x01\x00\x00".encode("ascii"))185195 """Converts version string into an integer. 196 197 :param version_str: The version string. 198 :type version_str: str 199 :return: A version integer. 200 201 >>> BsaFormat.version_number('103') 202 103 203 >>> BsaFormat.version_number('XXX') 204 -1 205 """ 206 try: 207 return int(version_str) 208 except ValueError: 209 # not supported 210 return -1211213 """A class to contain the actual bsa data.""" 214216 """Quickly checks if stream contains BSA data, and gets the 217 version, by looking at the first 8 bytes. 218 219 :param stream: The stream to inspect. 220 :type stream: file 221 """ 222 pos = stream.tell() 223 try: 224 self._version_value_.read(stream, data=self) 225 finally: 226 stream.seek(pos)227 228 # overriding pyffi.object_models.FileFormat.Data methods 229231 """Quickly checks if stream contains BSA data, and reads the 232 header. 233 234 :param stream: The stream to inspect. 235 :type stream: file 236 """ 237 pos = stream.tell() 238 try: 239 self.inspect_quick(stream) 240 BsaFormat._Header.read(self, stream, data=self) 241 finally: 242 stream.seek(pos)243245 """Read a bsa file. 246 247 :param stream: The stream from which to read. 248 :type stream: ``file`` 249 """ 250 logger = logging.getLogger("pyffi.bsa.data") 251 252 # inspect 253 self.inspect_quick(stream) 254 255 # read file 256 logger.debug("Reading header at 0x%08X." % stream.tell()) 257 BsaFormat._Header.read(self, stream, data=self) 258 if self.version == 0: 259 # morrowind 260 logger.debug("Reading file records at 0x%08X." % stream.tell()) 261 self.old_files.read(stream, data=self) 262 logger.debug( 263 "Reading file name offsets at 0x%08X." % stream.tell()) 264 for old_file in self.old_files: 265 old_file._name_offset_value_.read(stream, data=self) 266 logger.debug("Reading file names at 0x%08X." % stream.tell()) 267 for old_file in self.old_files: 268 old_file._name_value_.read(stream, data=self) 269 logger.debug("Reading file hashes at 0x%08X." % stream.tell()) 270 for old_file in self.old_files: 271 old_file._name_hash_value_.read(stream, data=self) 272 # "read" the files 273 logger.debug( 274 "Seeking end of raw file data at 0x%08X." % stream.tell()) 275 total_num_bytes = 0 276 for old_file in self.old_files: 277 total_num_bytes += old_file.data_size 278 stream.seek(total_num_bytes, os.SEEK_CUR) 279 else: 280 # oblivion and up 281 logger.debug( 282 "Reading folder records at 0x%08X." % stream.tell()) 283 self.folders.read(stream, data=self) 284 logger.debug( 285 "Reading folder names and file records at 0x%08X." 286 % stream.tell()) 287 for folder in self.folders: 288 folder._name_value_.read(stream, data=self) 289 folder._files_value_.read(stream, data=self) 290 logger.debug("Reading file names at 0x%08X." % stream.tell()) 291 for folder in self.folders: 292 for file_ in folder.files: 293 file_._name_value_.read(stream, data=self) 294 # "read" the files 295 logger.debug( 296 "Seeking end of raw file data at 0x%08X." % stream.tell()) 297 total_num_bytes = 0 298 for folder in self.folders: 299 for file_ in folder.files: 300 total_num_bytes += file_.file_size.num_bytes 301 stream.seek(total_num_bytes, os.SEEK_CUR) 302 303 # check if we are at the end of the file 304 if stream.read(1): 305 raise ValueError( 306 'end of file not reached: corrupt bsa file?')307
Home | Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Mon Oct 10 19:04:07 2011 | http://epydoc.sourceforge.net |