From 9eef4afab1df428a0b6e9690051ce0b44f10fb28 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Wed, 24 Jun 2026 07:01:47 -0500 Subject: [PATCH] Fix improper uninstall of plugins --- util/tl_inspector/TLInspector.bat | 34 ++++++++++-- util/tl_inspector/installer.py | 86 +++++++++++++++++++++++++++---- 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/util/tl_inspector/TLInspector.bat b/util/tl_inspector/TLInspector.bat index 1085807..772167c 100644 --- a/util/tl_inspector/TLInspector.bat +++ b/util/tl_inspector/TLInspector.bat @@ -49,18 +49,42 @@ $utf8 = New-Object System.Text.UTF8Encoding($false) # no BOM $content = [IO.File]::ReadAllText($pluginsJs) $nl = if ($content -match "`r`n") { "`r`n" } else { "`n" } $declared = [regex]::IsMatch($content, '"name"\s*:\s*"TLInspector"') +$hasRemnants = [regex]::IsMatch($content, '"description"\s*:\s*"TL source inspector"') $fileThere = Test-Path -LiteralPath $target # --- Already installed -> offer to remove ----------------------------------- -if ($declared -or $fileThere) { +if ($declared -or $hasRemnants -or $fileThere) { Write-Host "" Write-Host "TLInspector is currently INSTALLED." -ForegroundColor Yellow $ans = Read-Host "Remove it? (Y/N)" if ($ans -match '^(y|yes)$') { - if ($declared) { - $kept = ($content -split "`r?`n") | Where-Object { $_ -notmatch '"name"\s*:\s*"TLInspector"' } - $newText = ($kept -join $nl) - # drop a now-dangling comma before the closing ]; + if ($declared -or $hasRemnants) { + $newText = $content + while ($true) { + $m = [regex]::Match($newText, '"name"\s*:\s*"TLInspector"') + if (-not $m.Success) { break } + $start = $newText.LastIndexOf('{', $m.Index) + if ($start -lt 0) { break } + $depth = 0 + $end = -1 + for ($i = $start; $i -lt $newText.Length; $i++) { + if ($newText[$i] -eq '{') { $depth++ } + elseif ($newText[$i] -eq '}') { + $depth-- + if ($depth -eq 0) { $end = $i + 1; break } + } + } + if ($end -lt 0) { break } + while ($end -lt $newText.Length -and $newText[$end] -match '\s') { $end++ } + if ($end -lt $newText.Length -and $newText[$end] -eq ',') { $end++ } + $before = $newText.Substring(0, $start).TrimEnd() + $after = $newText.Substring($end).TrimStart() + if ($before.EndsWith(',')) { $before = $before.Substring(0, $before.Length - 1).TrimEnd() } + elseif ($after.StartsWith(',')) { $after = $after.Substring(1).TrimStart() } + $newText = $before + $after + } + $newText = [regex]::Replace($newText, ',\s*\{\s*"status"\s*:\s*true\s*,\s*"description"\s*:\s*"TL source inspector"\s*,\s*"parameters"\s*:\s*\{[^{}]*\}\s*\}', '') + $newText = [regex]::Replace($newText, '\{\s*"status"\s*:\s*true\s*,\s*"description"\s*:\s*"TL source inspector"\s*,\s*"parameters"\s*:\s*\{[^{}]*\}\s*\}\s*,?', '') $newText = [regex]::Replace($newText, ',(\s*)\];', '$1];') [IO.File]::WriteAllText($pluginsJs, $newText, $utf8) Write-Host "Removed the TLInspector entry from plugins.js" diff --git a/util/tl_inspector/installer.py b/util/tl_inspector/installer.py index c9da79e..3c88558 100644 --- a/util/tl_inspector/installer.py +++ b/util/tl_inspector/installer.py @@ -44,6 +44,78 @@ def _is_declared(content: str) -> bool: return bool(re.search(r'"name"\s*:\s*"TLInspector"', content)) +def _has_remnants(content: str) -> bool: + return bool( + re.search( + r'"description"\s*:\s*"TL source inspector"', + content, + ) + ) + + +def _find_plugin_block_span(content: str, plugin_name: str = PLUGIN_NAME) -> tuple[int, int] | None: + """Return [start, end) span of the plugin object in plugins.js, or None.""" + m = re.search(rf'"name"\s*:\s*"{re.escape(plugin_name)}"', content) + if not m: + return None + start = content.rfind("{", 0, m.start()) + if start < 0: + return None + depth = 0 + for i in range(start, len(content)): + ch = content[i] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + end = i + 1 + while end < len(content) and content[end] in " \t\r\n": + end += 1 + if end < len(content) and content[end] == ",": + end += 1 + return start, end + return None + + +def _remove_plugin_block(content: str, plugin_name: str = PLUGIN_NAME) -> str: + compact = rf"\{{[^{{}}]*\"name\"\s*:\s*\"{re.escape(plugin_name)}\"[^{{}}]*\}}\s*,?" + content = re.sub(compact, "", content) + while True: + span = _find_plugin_block_span(content, plugin_name) + if span is None: + break + start, end = span + before = content[:start].rstrip() + after = content[end:].lstrip() + if before.endswith(","): + before = before[:-1].rstrip() + elif after.startswith(","): + after = after[1:].lstrip() + gap = "\n" if before and after and not before.endswith("\n") else "" + content = before + gap + after + lines = [ + line + for line in re.split(r"\r?\n", content) + if not re.search(rf"\"name\"\s*:\s*\"{re.escape(plugin_name)}\"", line) + ] + content = "\n".join(lines) + # Leftover fragments from a previously corrupted multi-line entry. + content = re.sub( + r",\s*\{\s*\"status\"\s*:\s*true\s*,\s*\"description\"\s*:\s*\"TL source inspector\"" + r"\s*,\s*\"parameters\"\s*:\s*\{[^{}]*\}\s*\}", + "", + content, + ) + content = re.sub( + r"\{\s*\"status\"\s*:\s*true\s*,\s*\"description\"\s*:\s*\"TL source inspector\"" + r"\s*,\s*\"parameters\"\s*:\s*\{[^{}]*\}\s*\}\s*,?", + "", + content, + ) + return content + + def status(game_root: Path) -> dict: """Return install state for the game folder.""" info = detect_engine(game_root) @@ -123,16 +195,12 @@ def uninstall(game_root: Path) -> tuple[bool, str]: _, plugins_js, plugins_dir = info target = plugins_dir / f"{PLUGIN_NAME}.js" - content, nl = _read_plugins_js(plugins_js) + content, _ = _read_plugins_js(plugins_js) - if _is_declared(content): - kept = [ - line for line in re.split(r"\r?\n", content) - if not re.search(r'"name"\s*:\s*"TLInspector"', line) - ] - new_text = nl.join(kept) - new_text = re.sub(r",(\s*)\];", r"\1];", new_text) - plugins_js.write_text(new_text, encoding="utf-8", newline="") + if _is_declared(content) or _has_remnants(content): + content = _remove_plugin_block(content) + content = re.sub(r",(\s*)\];", r"\1];", content) + plugins_js.write_text(content, encoding="utf-8", newline="") if target.is_file(): target.unlink()