Files
2026-07-28 06:34:41 +02:00

150 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import re
import shutil
import sys
from pathlib import Path, PurePosixPath
from typing import Dict
from xml.sax.saxutils import quoteattr, unescape
Tree = Dict[str, "Tree"]
def fail(message: str) -> "NoReturn":
print(f"ERROR: {message}", file=sys.stderr)
raise SystemExit(1)
def build_directory_tree(target_paths: list[str]) -> Tree:
tree: Tree = {}
for raw_path in target_paths:
decoded = unescape(raw_path).replace("\\", "/")
parts = [
part
for part in PurePosixPath(decoded).parts[:-1]
if part not in ("", ".", "/")
]
node = tree
for part in parts:
node = node.setdefault(part, {})
return tree
def emit_directories(tree: Tree, depth: int = 2) -> list[str]:
lines: list[str] = []
indent = "\t" * depth
for name in sorted(tree, key=str.casefold):
children = tree[name]
attribute = quoteattr(name)
if children:
lines.append(f'{indent}<dir targ_name={attribute}>')
lines.extend(emit_directories(children, depth + 1))
lines.append(f"{indent}</dir>")
else:
lines.append(f'{indent}<dir targ_name={attribute} />')
return lines
def collect_declared_directories(tree: Tree, prefix: tuple[str, ...] = ()) -> set[str]:
result: set[str] = set()
for name, children in tree.items():
current = prefix + (name,)
result.add("/".join(current))
result.update(collect_declared_directories(children, current))
return result
def collect_required_directories(target_paths: list[str]) -> set[str]:
result: set[str] = set()
for raw_path in target_paths:
decoded = unescape(raw_path).replace("\\", "/")
parts = [
part
for part in PurePosixPath(decoded).parts[:-1]
if part not in ("", ".", "/")
]
for index in range(1, len(parts) + 1):
result.add("/".join(parts[:index]))
return result
def main() -> None:
gp4_path = Path(sys.argv[1] if len(sys.argv) >= 2 else "pkg.gp4")
if not gp4_path.is_file():
fail(f"no existe {gp4_path}")
text = gp4_path.read_text(encoding="utf-8")
target_paths = re.findall(
r'<file\b[^>]*\btarg_path="([^"]+)"',
text,
flags=re.IGNORECASE,
)
if not target_paths:
fail("el GP4 no contiene entradas <file targ_path=...>")
directory_tree = build_directory_tree(target_paths)
required = collect_required_directories(target_paths)
declared = collect_declared_directories(directory_tree)
missing = sorted(required - declared)
if missing:
fail(
"fallo interno al construir el árbol: "
+ ", ".join(missing)
)
rootdir_lines = ["\t<rootdir>"]
rootdir_lines.extend(emit_directories(directory_tree))
rootdir_lines.append("\t</rootdir>")
replacement = "\n".join(rootdir_lines)
rootdir_pattern = re.compile(
r"<rootdir\b[^>]*(?:/>|>.*?</rootdir>)",
flags=re.IGNORECASE | re.DOTALL,
)
updated, count = rootdir_pattern.subn(
lambda _: replacement,
text,
count=1,
)
if count != 1:
fail("no pude localizar exactamente un bloque <rootdir>")
backup_path = gp4_path.with_name(
gp4_path.name + ".before-rootdir-fix"
)
if not backup_path.exists():
shutil.copy2(gp4_path, backup_path)
gp4_path.write_text(updated, encoding="utf-8")
print(f"SUCCESS: reconstruido el árbol <rootdir> de {gp4_path}")
print(f"Backup : {backup_path}")
print(f"Files : {len(target_paths)}")
print(f"Dirs : {len(declared)}")
for directory in sorted(declared, key=str.casefold):
print(f" {directory}")
if __name__ == "__main__":
main()