85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Parse _ExtensionsInfo._ExtensionZippedInfo blobs.
|
|
|
|
Observed structure starts with a 4-byte marker, followed by a 20-byte SHA1 key
|
|
that points to a ConfigCAS file. The rest contains small extension metadata,
|
|
including a UTF-16LE serialized text fragment.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from inspect_1c_sql_files import GUID_RE
|
|
|
|
|
|
def extract_utf16le_fragment(data: bytes) -> str:
|
|
starts = [pos for pos in (data.find(b"{\x00"), data.find(b'"\x00#\x00"\x00')) if pos >= 0]
|
|
if not starts:
|
|
return ""
|
|
start = min(starts)
|
|
fragment = data[start:]
|
|
if len(fragment) % 2:
|
|
fragment = fragment[:-1]
|
|
try:
|
|
return fragment.decode("utf-16-le", errors="ignore").strip("\x00")
|
|
except UnicodeError:
|
|
return ""
|
|
|
|
|
|
def parse_blob(path: Path, cas_dir: Path | None) -> dict[str, Any]:
|
|
data = path.read_bytes()
|
|
root_key = data[4:24].hex() if len(data) >= 24 else ""
|
|
text = extract_utf16le_fragment(data)
|
|
cas_path = cas_dir / root_key if cas_dir and root_key else None
|
|
return {
|
|
"file_name": path.name,
|
|
"path": str(path),
|
|
"bytes": len(data),
|
|
"marker_hex": data[:4].hex(),
|
|
"root_cas_key": root_key,
|
|
"root_cas_exists": bool(cas_path and cas_path.is_file()),
|
|
"root_cas_path": str(cas_path) if cas_path and cas_path.is_file() else None,
|
|
"text_fragment": text,
|
|
"guids": sorted(set(match.lower() for match in GUID_RE.findall(text))),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Parse exported _ExtensionZippedInfo blobs.")
|
|
parser.add_argument("input", type=Path, help="A blob file or directory with *_ExtensionZippedInfo.bin files.")
|
|
parser.add_argument("--cas-dir", type=Path)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
|
|
files = [args.input] if args.input.is_file() else sorted(args.input.glob("*ExtensionZippedInfo.bin"))
|
|
items = [parse_blob(path, args.cas_dir) for path in files]
|
|
report = {
|
|
"schema": "onec_extension_zipped_info.v1",
|
|
"input": str(args.input),
|
|
"cas_dir": str(args.cas_dir) if args.cas_dir else None,
|
|
"file_count": len(items),
|
|
"root_cas_found_count": sum(1 for item in items if item["root_cas_exists"]),
|
|
"items": items,
|
|
}
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"output": str(args.output),
|
|
"files": len(items),
|
|
"root_cas_found": report["root_cas_found_count"],
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|