| 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 EdgeFilter
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
133
135
137 return 2 + len(self._value)
138
140 length, = struct.unpack('<B', stream.read(1))
141 self._value = stream.read(length)[:-1] # strip trailing null byte
142
147
149 """Basic type which implements the header of a BSA file."""
150
153
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
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
192
193 @staticmethod
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
213 """A class to contain the actual bsa data."""
214
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
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
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
316
317 if __name__ == '__main__':
318 import doctest
319 doctest.testmod()
320
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Mon Oct 10 19:04:07 2011 | http://epydoc.sourceforge.net |