42 lines
2.0 KiB
Python
42 lines
2.0 KiB
Python
"""Read an XMind app bundle's version and sample files without executing its code.
|
|
Usage: python3 scripts/flow-board/inspect-xmind-package.py /Volumes/Xmind/Xmind.app --samples-dir /tmp/xmind-native-samples
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import plistlib
|
|
import struct
|
|
import zipfile
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("app", type=Path)
|
|
parser.add_argument("--samples-dir", type=Path)
|
|
args = parser.parse_args()
|
|
info = plistlib.loads((args.app / "Contents/Info.plist").read_bytes())
|
|
report = {"version": info["CFBundleShortVersionString"], "build": info["CFBundleVersion"], "bundleId": info["CFBundleIdentifier"], "samples": []}
|
|
with (args.app / "Contents/Resources/app.asar").open("rb") as archive:
|
|
_, header_size, _, string_size = struct.unpack("<4I", archive.read(16))
|
|
header = json.loads(archive.read(string_size))
|
|
def walk(directory, prefix=""):
|
|
for name, item in directory.get("files", {}).items():
|
|
path = prefix + "/" + name
|
|
if "files" in item:
|
|
yield from walk(item, path)
|
|
elif name.endswith(".xmind"):
|
|
yield path, item
|
|
for name, item in walk(header):
|
|
archive.seek(8 + header_size + int(item["offset"]))
|
|
payload = archive.read(item["size"])
|
|
sample = {"path": name, "bytes": len(payload), "sha256": hashlib.sha256(payload).hexdigest()}
|
|
with zipfile.ZipFile(io.BytesIO(payload)) as workbook:
|
|
if "content.json" in workbook.namelist():
|
|
sheets = json.loads(workbook.read("content.json"))
|
|
sample["structures"] = [sheet["rootTopic"].get("structureClass") for sheet in sheets]
|
|
report["samples"].append(sample)
|
|
if args.samples_dir and "/basic/" in name:
|
|
args.samples_dir.mkdir(parents=True, exist_ok=True)
|
|
(args.samples_dir / Path(name).name).write_bytes(payload)
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|