Exemple #1
0
    def _store_file(self, fobj: io.BufferedIOBase) -> typing.Iterable[bytes]:
        need_compression = self._comp_cfg is not None
        orig_size = None
        if need_compression and fobj.seekable():
            orig_size = self._get_size_till_eof(fobj)
            if orig_size < self._comp_cfg.min_size:
                need_compression = False

        need_encryption = self._enc_cfg is not None
        yield self._make_header(comp=need_compression, enc=need_encryption)

        chunks = self._iter_file_chunk(fobj, self._chunk_size)
        if need_compression:
            chunks = self._compress(chunks, orig_size)
        if need_encryption:
            chunks = self._encrypt(chunks)
        yield from chunks
 def __init__(self, stream: io.BufferedIOBase):
     if not isinstance(stream, io.BufferedIOBase):
         raise TypeError("need BufferedIOBase")
     self.stream = stream
     assert stream.isatty() is False and stream.seekable() is True
Exemple #3
0
	def __init__(self, stream: io.BufferedIOBase, new: Version = None):
		"""Parse a MIX from `stream`, which must be a buffered file object.
		
		If `new` is not None, initialize an empty MIX of this version instead.
		MixParseError is raised on parsing errors.
		"""
		
		# Initialize mandatory attributes
		self._dirty = False
		self._stream = None
		self._open = []
		
		# If stream is, for example, a raw I/O object, files could be destroyed
		# without ever raising an error, so check this.
		if not isinstance(stream, io.BufferedIOBase):
			raise TypeError("`stream` must be an instance of io.BufferedIOBase")
		
		if not stream.readable():
			raise ValueError("`stream` must be readable")
		
		if not stream.seekable():
			raise ValueError("`stream` must be seekable")
		
		if new is not None:
			# Start empty (new file)
			if type(new) is not Version:
				raise TypeError("`new` must be a Version enumeration member or None")
			if version is Version.RG:
				raise NotImplementedError("RG MIX files are not yet supported")
			self._stream = stream
			self._index = {}
			self._contents = []
			self._version = version
			self._flags = 0
			return
		
		# Parse an existing file
		filesize = stream.seek(0, io.SEEK_END)
		if filesize <= 6:
			raise MixParseError("File too small")
		stream.seek(0)
		
		first4 = stream.read(4)
		if first4 == b"MIX1":
			raise NotImplementedError("RG MIX files are not yet supported")
		elif first4[:2] == b"\x00\x00":
			# It seems we have a RA or TS MIX so check the flags
			flags = int.from_bytes(first4[2:], "little")
			if flags > 3:
				raise MixParseError("Unsupported properties")
			if flags & 2:
				raise NotImplementedError("Encrypted MIX files are not yet supported")
			
			# FIXME HERE: 80 bytes of westwood key_source if encrypted,
			#             to create a 56 byte long blowfish key from it.
			#
			#             They are followed by a number of 8 byte blocks,
			#             the first of them decrypting to filecount and bodysize.
			
			# Encrypted TS MIXes have a key.ini we can check for later,
			# so at this point assume Version.TS only if unencrypted.
			# Stock RA MIXes seem to be always encrypted.
			version = Version.TS
			
			# RA/TS MIXes hold their filecount after the flags,
			# whilst for TD MIXes their first two bytes are the filecount.
			filecount = int.from_bytes(stream.read(2), "little")
		else:
			version = Version.TD
			flags = 0
			filecount = int.from_bytes(first4[:2], "little")
			stream.seek(2)
			
		# From here it's the same for every unencrypted MIX
		bodysize    = int.from_bytes(stream.read(4), "little")
		indexoffset = stream.tell()
		indexsize   = filecount * 12
		bodyoffset  = indexoffset + indexsize

		# Check if data is sane
		# FIXME: Checksummed MIXes have 20 additional bytes after the body.
		if filesize - bodyoffset != bodysize:
			raise MixParseError("Incorrect filesize or invalid header")

		# OK, time to read the index
		index = {}
		for key, offset, size in struct.iter_unpack("<LLL", stream.read(indexsize)):
			offset += bodyoffset
			
			if offset + size > filesize:
				raise MixParseError("Content extends beyond end of file")

			index[key] = _MixNode(key, offset, size, size, None)

		if len(index) != filecount:
			raise MixParseError("Duplicate key")

		# Now read the names
		# TD/RA: 1422054725; TS: 913179935
		for dbkey in (1422054725, 913179935):
			if dbkey in index:
				stream.seek(index[dbkey].offset)
				header = stream.read(32)

				if header != b"XCC by Olaf van der Spek\x1a\x04\x17'\x10\x19\x80\x00":
					continue

				dbsize  = int.from_bytes(stream.read(4), "little")  # Total filesize

				if dbsize != index[dbkey].size or dbsize > 16777216:
					raise MixParseError("Invalid name table")

				# Skip four bytes for XCC type; 0 for LMD, 2 for XIF
				# Skip four bytes for DB version; Always zero
				stream.seek(8, io.SEEK_CUR)
				gameid = int.from_bytes(stream.read(4), "little")  # XCC Game ID
				
				# XCC saves alias numbers, so converting them
				# to `Version` is not straight forward.
				# FIXME: Check if Dune games and Nox also use MIX files
				if gameid == 0:
					if version is not Version.TD:
						continue
				elif gameid == 1:
					version = Version.RA
				elif 2 <= gameid <= 6 or gameid == 15:
					version = Version.TS
				else:
					continue
				
				namecount = int.from_bytes(stream.read(4), "little")
				bodysize  = dbsize - 53  # Size - Header - Last byte
				namelist  = stream.read(bodysize).split(b"\x00") if bodysize else []
				
				if len(namelist) != namecount:
					raise MixParseError("Invalid name table")
				
				# Remove Database from index
				del index[dbkey]
				
				# Add names to index
				names = False
				for name in namelist:
					name = name.decode(ENCODING, "ignore")
					key = genkey(name, version)
					if key in index:
						index[key].name = name
						names = True
				
				# XCC sometimes puts two Databases in a file by mistake,
				# so if no names were found, give it another try
				if names: break

		# Create a sorted list of all contents
		contents = sorted(index.values(), key=lambda node: node.offset)

		# Calculate alloc values
		# This is the size up to wich a file may grow without needing a move
		for i in range(len(contents) - 1):
			contents[i].alloc = contents[i+1].offset - contents[i].offset

			if contents[i].alloc < contents[i].size:
				raise MixParseError("Overlapping file boundaries")

		# Populate the object
		self._stream = stream
		self._version = version
		self._index = index
		self._contents = contents
		self._flags = flags