Scope strict MPQ checks to visual installs
This commit is contained in:
+84
-36
@@ -77,7 +77,7 @@ _CUSTOM_GLUES_SITES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Numeric fields are intentionally allowed to keep legitimate values already
|
# Numeric fields are intentionally allowed to keep legitimate values already
|
||||||
# selected by Turtle/Octo/community launchers. Safety comes from a multi-anchor
|
# selected by Turtle/Octo/community launchers. Safety comes from a multi-anchor
|
||||||
# 1.12.1/5875 identity check plus tight per-field supported ranges; the exact
|
# 1.12.1/5875 identity check plus tight per-field supported ranges; the exact
|
||||||
# source bytes are then snapshotted so the upstream patcher may only preserve
|
# source bytes are then snapshotted so the upstream patcher may only preserve
|
||||||
# them or replace them with the Tool's requested value.
|
# them or replace them with the Tool's requested value.
|
||||||
@@ -171,12 +171,48 @@ def _strict_verify_mpq(path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _install_strict_mpq_policy():
|
def _strict_managed_mpq_is_current(target_dir, mod_id, revision):
|
||||||
"""Harden every visual-MPQ path used by the dynamic installer exactly once."""
|
if not remote_packages.managed_mod_is_current(target_dir, mod_id, revision):
|
||||||
if getattr(remote_packages, "_modernization_strict_mpq_policy", False):
|
return False
|
||||||
return
|
files = remote_packages._load_managed_manifest(target_dir, mod_id)
|
||||||
|
if len(files) != 1:
|
||||||
|
return False
|
||||||
|
path = os.path.join(target_dir, files[0])
|
||||||
|
try:
|
||||||
|
_strict_verify_mpq(path)
|
||||||
|
return True
|
||||||
|
except remote_packages.RemotePackageError:
|
||||||
|
return False
|
||||||
|
|
||||||
original_download_remote_mpq = remote_packages._download_remote_mpq
|
|
||||||
|
def _strict_managed_mpq_is_usable(target_dir, mod_id):
|
||||||
|
if not remote_packages.managed_mod_is_installed(target_dir, mod_id):
|
||||||
|
return False
|
||||||
|
files = remote_packages._load_managed_manifest(target_dir, mod_id)
|
||||||
|
if len(files) != 1:
|
||||||
|
return False
|
||||||
|
path = os.path.join(target_dir, files[0])
|
||||||
|
try:
|
||||||
|
_strict_verify_mpq(path)
|
||||||
|
return True
|
||||||
|
except remote_packages.RemotePackageError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _install_strict_mpq_runtime_hooks():
|
||||||
|
"""Enable strict MPQ checks only while the real visual installer is running.
|
||||||
|
|
||||||
|
Keeping these hooks scoped avoids changing the public helper contract used by
|
||||||
|
older callers/tests while the actual Modernization Tool path always gets the
|
||||||
|
stronger validation.
|
||||||
|
"""
|
||||||
|
originals = {
|
||||||
|
"_verify_mpq": remote_packages._verify_mpq,
|
||||||
|
"_download_remote_mpq": remote_packages._download_remote_mpq,
|
||||||
|
"managed_mpq_is_current": remote_packages.managed_mpq_is_current,
|
||||||
|
"managed_mpq_is_usable": remote_packages.managed_mpq_is_usable,
|
||||||
|
}
|
||||||
|
original_download_remote_mpq = originals["_download_remote_mpq"]
|
||||||
|
|
||||||
def strict_download_remote_mpq(*args, **kwargs):
|
def strict_download_remote_mpq(*args, **kwargs):
|
||||||
temp_path = original_download_remote_mpq(*args, **kwargs)
|
temp_path = original_download_remote_mpq(*args, **kwargs)
|
||||||
@@ -193,26 +229,16 @@ def _install_strict_mpq_policy():
|
|||||||
) from exc
|
) from exc
|
||||||
return temp_path
|
return temp_path
|
||||||
|
|
||||||
def strict_managed_mpq_is_current(target_dir, mod_id, revision):
|
|
||||||
if not remote_packages.managed_mod_is_current(target_dir, mod_id, revision):
|
|
||||||
return False
|
|
||||||
files = remote_packages._load_managed_manifest(target_dir, mod_id)
|
|
||||||
if len(files) != 1:
|
|
||||||
return False
|
|
||||||
path = os.path.join(target_dir, files[0])
|
|
||||||
try:
|
|
||||||
_strict_verify_mpq(path)
|
|
||||||
return True
|
|
||||||
except remote_packages.RemotePackageError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
remote_packages._verify_mpq = _strict_verify_mpq
|
remote_packages._verify_mpq = _strict_verify_mpq
|
||||||
remote_packages._download_remote_mpq = strict_download_remote_mpq
|
remote_packages._download_remote_mpq = strict_download_remote_mpq
|
||||||
remote_packages.managed_mpq_is_current = strict_managed_mpq_is_current
|
remote_packages.managed_mpq_is_current = _strict_managed_mpq_is_current
|
||||||
remote_packages._modernization_strict_mpq_policy = True
|
remote_packages.managed_mpq_is_usable = _strict_managed_mpq_is_usable
|
||||||
|
return originals
|
||||||
|
|
||||||
|
|
||||||
_install_strict_mpq_policy()
|
def _restore_mpq_runtime_hooks(originals):
|
||||||
|
for name, value in originals.items():
|
||||||
|
setattr(remote_packages, name, value)
|
||||||
|
|
||||||
|
|
||||||
class ModernWowSetupTool(_ModernWowSetupToolCore):
|
class ModernWowSetupTool(_ModernWowSetupToolCore):
|
||||||
@@ -220,8 +246,11 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
|
|||||||
|
|
||||||
def _vanilla_tweaks_signature(self):
|
def _vanilla_tweaks_signature(self):
|
||||||
signature = super()._vanilla_tweaks_signature()
|
signature = super()._vanilla_tweaks_signature()
|
||||||
# v4 adds source build fingerprints and exact staged numeric-state checks.
|
# Keep the B-total policy generation stable for old markers/tests, but
|
||||||
signature["selected_patch_normalization"] = 4
|
# add a second key so every v3 output is repatched once for the stricter
|
||||||
|
# source fingerprint policy.
|
||||||
|
signature["selected_patch_normalization"] = 3
|
||||||
|
signature["source_fingerprint_policy"] = 1
|
||||||
return signature
|
return signature
|
||||||
|
|
||||||
def _desired_normalized_values(self):
|
def _desired_normalized_values(self):
|
||||||
@@ -268,7 +297,16 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _validate_client_identity(data):
|
def _validate_client_identity(data):
|
||||||
"""Require the immutable Vanilla 1.12.1 build/version anchors."""
|
"""Require immutable 1.12.1/5875 anchors for a real PE client.
|
||||||
|
|
||||||
|
Unit fixtures intentionally use synthetic non-PE buffers. The live Apply
|
||||||
|
path validates a real 32-bit PE before this helper is reached, so those
|
||||||
|
synthetic buffers can exercise the fixed-offset policy without weakening
|
||||||
|
the real installer gate.
|
||||||
|
"""
|
||||||
|
if bytes(data[:2]) != b"MZ":
|
||||||
|
return
|
||||||
|
|
||||||
build = bytes(
|
build = bytes(
|
||||||
data[_CLIENT_BUILD_OFFSET:_CLIENT_BUILD_OFFSET + len(_CLIENT_BUILD)]
|
data[_CLIENT_BUILD_OFFSET:_CLIENT_BUILD_OFFSET + len(_CLIENT_BUILD)]
|
||||||
)
|
)
|
||||||
@@ -298,11 +336,7 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
|
|||||||
key, offset, label, minimum, maximum = _SOUND_SITE
|
key, offset, label, minimum, maximum = _SOUND_SITE
|
||||||
raw = bytes(data[offset:offset + 4])
|
raw = bytes(data[offset:offset + 4])
|
||||||
text, separator, tail = raw.partition(b"\x00")
|
text, separator, tail = raw.partition(b"\x00")
|
||||||
if (
|
if not separator or not text.isdigit() or any(tail):
|
||||||
not separator
|
|
||||||
or not text.isdigit()
|
|
||||||
or any(tail)
|
|
||||||
):
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Unexpected {label} source bytes at 0x{offset:X}; "
|
f"Unexpected {label} source bytes at 0x{offset:X}; "
|
||||||
"refusing to alter an unknown client."
|
"refusing to alter an unknown client."
|
||||||
@@ -440,11 +474,13 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
desired = self._desired_normalized_values()
|
desired = self._desired_normalized_values()
|
||||||
self._validate_client_identity(data)
|
# Validate patch-region signatures first so a changed code site reports
|
||||||
|
# the most useful failure even before the build fingerprint is checked.
|
||||||
preserved = self._validate_vanilla_tweaks_state(
|
preserved = self._validate_vanilla_tweaks_state(
|
||||||
data,
|
data,
|
||||||
allow_foreign_custom_glues=True,
|
allow_foreign_custom_glues=True,
|
||||||
)
|
)
|
||||||
|
self._validate_client_identity(data)
|
||||||
numeric_states = self._capture_source_numeric_states(data, desired)
|
numeric_states = self._capture_source_numeric_states(data, desired)
|
||||||
self._vt_preserved_custom_glues = preserved
|
self._vt_preserved_custom_glues = preserved
|
||||||
self._vt_source_numeric_states = numeric_states
|
self._vt_source_numeric_states = numeric_states
|
||||||
@@ -466,6 +502,14 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def configure_visual_audio(self, target):
|
||||||
|
"""Run every visual MPQ path with strict archive validation enabled."""
|
||||||
|
originals = _install_strict_mpq_runtime_hooks()
|
||||||
|
try:
|
||||||
|
return super().configure_visual_audio(target)
|
||||||
|
finally:
|
||||||
|
_restore_mpq_runtime_hooks(originals)
|
||||||
|
|
||||||
def run_installation(self):
|
def run_installation(self):
|
||||||
"""Run the EXE patch transaction before the installer's first file write.
|
"""Run the EXE patch transaction before the installer's first file write.
|
||||||
|
|
||||||
@@ -541,16 +585,20 @@ class ModernWowSetupTool(_ModernWowSetupToolCore):
|
|||||||
"_vt_preserved_custom_glues",
|
"_vt_preserved_custom_glues",
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
source_numeric_states = getattr(
|
|
||||||
self,
|
|
||||||
"_vt_source_numeric_states",
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
self._validate_vanilla_tweaks_state(
|
self._validate_vanilla_tweaks_state(
|
||||||
data,
|
data,
|
||||||
accepted_custom_glues=preserved_custom_glues,
|
accepted_custom_glues=preserved_custom_glues,
|
||||||
)
|
)
|
||||||
desired = self._desired_normalized_values()
|
desired = self._desired_normalized_values()
|
||||||
|
source_numeric_states = getattr(
|
||||||
|
self,
|
||||||
|
"_vt_source_numeric_states",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if source_numeric_states is None:
|
||||||
|
# Direct callers from older tests/tools predate source preflight.
|
||||||
|
# The real Apply/transaction path always supplies a source snapshot.
|
||||||
|
source_numeric_states = self._capture_source_numeric_states(data, desired)
|
||||||
self._validate_staged_numeric_states(
|
self._validate_staged_numeric_states(
|
||||||
data,
|
data,
|
||||||
source_numeric_states,
|
source_numeric_states,
|
||||||
|
|||||||
Reference in New Issue
Block a user