Package pyffi :: Package formats :: Package bsa
[hide private]
[frames] | no frames]

Source Code for Package pyffi.formats.bsa

  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 EdgeFilter 
109 110 111 -class BsaFormat(pyffi.object_models.xml.FileFormat):
112 """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
126 - class Hash(pyffi.object_models.common.UInt64):
127
128 - def __str__(self):
129 return "0x%016X" % self._value
130
131 - def get_detail_display(self):
132 return self.__str__()
133
134 - class BZString(pyffi.object_models.common.SizedString):
135
136 - def get_size(self, data=None):
137 return 2 + len(self._value)
138
139 - def read(self, stream, data=None):
140 length, = struct.unpack('<B', stream.read(1)) 141 self._value = stream.read(length)[:-1] # strip trailing null byte
142
143 - def write(self, stream, data=None):
144 stream.write(struct.pack('<B', len(self._value))) 145 stream.write(self._value) 146 stream.write(struct.pack('<B', 0))
147
148 - class FileVersion(pyffi.object_models.common.UInt):
149 """Basic type which implements the header of a BSA file.""" 150
151 - def __init__(self, **kwargs):
152 BasicBase.__init__(self, **kwargs)
153
154 - def read(self, stream, data):
155 """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)
173
174 - def write(self, stream, data):
175 """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"))
185
186 - def get_size(self, data=None):
187 """Return number of bytes the header string occupies in a file. 188 189 :return: Number of bytes. 190 """ 191 return 4
192 193 @staticmethod
194 - def version_number(version_str):
195 """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 -1
211
212 - class Header(pyffi.object_models.FileFormat.Data):
213 """A class to contain the actual bsa data.""" 214
215 - def inspect_quick(self, stream):
216 """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 229
230 - def inspect(self, stream):
231 """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)
243
244 - def read(self, stream):
245 """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
308 - def write(self, stream):
309 """Write a bsa file. 310 311 :param stream: The stream to which to write. 312 :type stream: ``file`` 313 """ 314 # write the file 315 raise NotImplementedError
316 317 if __name__ == '__main__': 318 import doctest 319 doctest.testmod() 320