"""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))