mirror of
https://github.com/EmiiBytee/np2kai-ps4.git
synced 2026-10-06 08:00:11 +02:00
150 lines
3.8 KiB
Python
Executable File
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()
|