Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user