Skip to content

Commit

Permalink
fix: QMOD error handling and tolerance (#71)
Browse files Browse the repository at this point in the history
* Add quite a bit of general QMOD error handling. Errors in QMODs are no longer reported as Launcher bugs.
* Handle when all files in a QMOD are wrapped within a top-level directory (this is not as specced, but has happened in the wild—possibly to do with the default behaviour of OSes/specific compression tools when multiple files are selected?)
* Ignore any qmod.ini files that aren't in the toppest level of the mod.

Fixes #70
  • Loading branch information
matatk authored Feb 12, 2021
1 parent 626c1c9 commit 46e2ef7
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 37 deletions.
1 change: 0 additions & 1 deletion audioquake/launcherlib/game_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ def launch_map(self, name, game=RootGame.ANY):
self.opts_custom_map_base + ('+map ' + str(name),), game=game)

def launch_mod(self, name):
print('Launching mod:', name)
return self._launch_core(('-game', name))

def quit(self):
Expand Down
2 changes: 1 addition & 1 deletion audioquake/launcherlib/ui/tabs/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def build_and_play(self, xmlfile, play_wad):
try:
self.build_and_copy(xmlfile, wad, destinations)
except LDLError:
Error(self, str(exc_info()[1]))
Error(self, str(exc_info()[1])) # TODO: Why not str(err)?
return

if play_wad:
Expand Down
11 changes: 8 additions & 3 deletions audioquake/launcherlib/ui/tabs/mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from launcherlib import dirs
from launcherlib.ui.helpers import \
add_widget, add_opener_button, pick_file, launch_core, \
Info, YesNoWithTitle
Info, YesNoWithTitle, Error

from qmodlib import QMODFile, InstalledQMOD
from qmodlib import QMODFile, InstalledQMOD, BadQMODFileError


class ModTab(wx.Panel):
Expand Down Expand Up @@ -37,7 +37,12 @@ def install_qmod_handler(self, event):
incoming = pick_file(
self, "Select a QMOD file", "QMOD files (*.qmod)|*.qmod")
if incoming:
qmod = QMODFile(incoming)
try:
qmod = QMODFile(incoming)
except BadQMODFileError as err:
Error(self, f'There is a problem with the QMOD file: {err}.')
return

title = qmod.name + ' ' + qmod.version
desc = qmod.shortdesc + '\n\n' + qmod.longdesc

Expand Down
107 changes: 75 additions & 32 deletions audioquake/qmodlib.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,110 @@
"""QMOD file handling"""
from configparser import ConfigParser
from os.path import commonprefix
from pathlib import Path
import shutil
from zipfile import ZipFile
from zipfile import ZipFile, BadZipFile


class BadQMODFileException(Exception):
class BadQMODFileError(Exception):
pass


class NoQMODDirectoryException(Exception):
class NoQMODDirectoryError(Exception):
pass


class BadQMODDirectoryException(Exception):
class BadQMODDirectoryError(Exception):
pass


def unwrap_and_embed_ini(archive, wrapper, gamedir):
offset = len(wrapper)
for info in archive.infolist():
name = info.filename
if len(name) > offset:
member = name[offset:]
if member == 'qmod.ini':
# Ensure INI gets extracted in the root
info.filename = gamedir + '/' + member
elif member.endswith('qmod.ini'):
# Some mods errantly include a duplicate
# TODO: Check for this if I ever write a checker
continue
else:
info.filename = member
yield info


class QMODFile():
def __init__(self, path):
zipfile = ZipFile(path, mode='r')
if zipfile.testzip() is not None:
raise BadQMODFileException("'" + path + "' failed zip test")
files = zipfile.namelist()
if 'qmod.ini' not in files:
raise BadQMODFileException("Missing 'qmod.ini'")
files.remove('qmod.ini')
try:
archive = ZipFile(path, mode='r')
except BadZipFile:
name = Path(path).name
raise BadQMODFileError(f"'{name}' is not in ZIP compressed format")

if archive.testzip() is not None:
raise BadQMODFileError(f"'{path}' failed zip test")

# Some QMODs wrap everything in a top-level directory, possibly due to
# the behaviour of OS ZIPing tools. This is not how QMODs are specced,
# but the following code can handle it.
wrapper = commonprefix(archive.namelist())

files = archive.namelist()
qmod_ini_name = wrapper + 'qmod.ini'
if qmod_ini_name not in files:
raise BadQMODFileError("Missing 'qmod.ini'")
files.remove(qmod_ini_name)

# FIXME check it's a dir
dirs = set([Path(x).parts[0] for x in files])
if len(dirs) > 1:
raise BadQMODFileException('Improper directory structure')

ini_string = zipfile.read('qmod.ini').decode('utf-8')
config = ConfigParser()
config.read_string(ini_string)

self.name = config['general']['name']
self.shortdesc = config['general']['shortdesc']
self.version = config['general']['version']
self.longdesc = ' '.join([line for line in config['longdesc'].values()])

self.gamedir = config['general']['gamedir']

self.datafiles = files
self.zipfile = zipfile
raise BadQMODFileError('Improper directory structure')

try:
ini_string = archive.read(qmod_ini_name).decode('utf-8')
except Exception as err:
raise BadQMODFileError(f"Couldn't read/decode 'qmod.ini': {err}")

try:
config = ConfigParser()
config.read_string(ini_string)
except Exception as err:
raise BadQMODFileError(f"'qmod.ini' is invalid: {err}")

try:
self.name = config['general']['name']
self.shortdesc = config['general']['shortdesc']
self.version = config['general']['version']
self.longdesc = ' '.join(
[line for line in config['longdesc'].values()])
self.gamedir = config['general']['gamedir']
# FIXME check gamedir matches above dir
except KeyError as err:
raise BadQMODFileError(f"'qmod.ini' is missing section/key '{err}'")

self.archive = archive
self.wrapper = wrapper

def install(self, root):
for datafile in self.datafiles:
self.zipfile.extract(datafile, path=root) # will be within gamedir

self.zipfile.extract('qmod.ini', path=root / self.gamedir)
self.archive.extractall(
path=root, # will be within gamedir
members=unwrap_and_embed_ini(self.archive, self.wrapper, self.gamedir))


class InstalledQMOD():
def __init__(self, name):
path = Path(name)

if not path.is_dir():
raise NoQMODDirectoryException()
raise NoQMODDirectoryError()

ini_path = path / 'qmod.ini'

if not ini_path.is_file():
raise BadQMODDirectoryException()
raise BadQMODDirectoryError()

config = ConfigParser()
config.read_file(open(ini_path))
Expand Down

0 comments on commit 46e2ef7

Please sign in to comment.