opencc-sys 0.5.0+1.4.0

OpenCC bindings for Rust
Documentation
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# This tool is intended to process resource ZIPs generated by
# opencc_resources_zip. It assumes that all .txt entries are OpenCC text
# dictionaries and that config JSON files only use "type": "text" / "file":
# "*.txt" for dictionary references.

import argparse
import hashlib
import json
import os
import subprocess
import tempfile
import zipfile

MANIFEST_NAME = "opencc-resource-manifest.json"


def parse_args():
    parser = argparse.ArgumentParser(
        description="Convert a text-backed resource zip to an ocd2-backed resource zip."
    )
    parser.add_argument(
        "--input", required=True, help="Input resource zip file (text-backed)"
    )
    parser.add_argument(
        "--output", required=True, help="Output resource zip file (ocd2-backed)"
    )
    parser.add_argument(
        "--opencc-dict", required=True, help="Path to opencc_dict executable"
    )
    return parser.parse_args()


def convert_dict_references(value):
    if isinstance(value, dict):
        converted = {}
        for key, child in value.items():
            if key == "type" and child == "text":
                converted[key] = "ocd2"
            elif (
                key == "file"
                and isinstance(child, str)
                and child.endswith(".txt")
            ):
                converted[key] = child[:-4] + ".ocd2"
            else:
                converted[key] = convert_dict_references(child)
        return converted
    if isinstance(value, list):
        return [convert_dict_references(child) for child in value]
    return value


def sha256_hash(data):
    return hashlib.sha256(data).hexdigest()


def main():
    args = parse_args()

    if not os.path.exists(args.input):
        raise FileNotFoundError(f"Input zip file not found: {args.input}")

    output_dir = os.path.dirname(args.output)
    if output_dir:
        os.makedirs(output_dir, exist_ok=True)

    with zipfile.ZipFile(args.input, "r") as in_zip:
        # Load manifest
        try:
            manifest_content = in_zip.read(MANIFEST_NAME).decode("utf-8")
        except KeyError:
            raise ValueError(
                f"Manifest file {MANIFEST_NAME} not found in the input zip"
            )

        manifest = json.loads(manifest_content)
        in_names = in_zip.namelist()

        new_entries = {}
        out_contents = {}  # filename -> bytes
        out_zipinfos = {}  # filename -> zipinfo

        with tempfile.TemporaryDirectory() as temp_dir:
            # Extract all .txt dictionary files
            txt_files = [name for name in in_names if name.endswith(".txt")]
            for name in txt_files:
                in_zip.extract(name, temp_dir)

            # Compile each .txt to .ocd2
            for name in txt_files:
                base_name = name[:-4]
                ocd2_name = base_name + ".ocd2"

                txt_path = os.path.join(temp_dir, name)
                ocd2_path = os.path.join(temp_dir, ocd2_name)

                # Run opencc_dict
                subprocess.run(
                    [
                        args.opencc_dict,
                        "-i",
                        txt_path,
                        "-o",
                        ocd2_path,
                        "-f",
                        "text",
                        "-t",
                        "ocd2",
                    ],
                    check=True,
                )

                # Read the compiled ocd2 data
                with open(ocd2_path, "rb") as f:
                    ocd2_data = f.read()

                out_contents[ocd2_name] = ocd2_data

                # Copy ZipInfo properties from the original file
                orig_info = in_zip.getinfo(name)
                info = zipfile.ZipInfo(ocd2_name)
                info.date_time = orig_info.date_time
                info.compress_type = zipfile.ZIP_STORED
                info.external_attr = 0o644 << 16
                out_zipinfos[ocd2_name] = info

                new_entries[ocd2_name] = {
                    "sha256": sha256_hash(ocd2_data),
                    "size": len(ocd2_data),
                }

            # Process config files (.json)
            json_files = [
                name
                for name in in_names
                if name.endswith(".json") and name != MANIFEST_NAME
            ]
            for name in json_files:
                config_content = in_zip.read(name).decode("utf-8")
                config = json.loads(config_content)
                converted_config = convert_dict_references(config)

                # Dump config using standard formatting matching opencc_resources_zip
                new_json_str = (
                    json.dumps(converted_config, ensure_ascii=False, indent=2)
                    + "\n"
                )
                new_json_bytes = new_json_str.encode("utf-8")

                out_contents[name] = new_json_bytes

                orig_info = in_zip.getinfo(name)
                info = zipfile.ZipInfo(name)
                info.date_time = orig_info.date_time
                info.compress_type = zipfile.ZIP_STORED
                info.external_attr = 0o644 << 16
                out_zipinfos[name] = info

                new_entries[name] = {
                    "sha256": sha256_hash(new_json_bytes),
                    "size": len(new_json_bytes),
                }

        # Build the new manifest preserving original metadata
        new_manifest = {
            "manifest_version": manifest.get("manifest_version", 1),
            "source_url": manifest.get("source_url", ""),
            "commit_id": manifest.get("commit_id", ""),
            "source_dirty": manifest.get("source_dirty", False),
            "build_time_utc": manifest.get("build_time_utc", ""),
            "hash_algorithm": "sha256",
            "entries": new_entries,
        }

        new_manifest_str = (
            json.dumps(
                new_manifest, ensure_ascii=False, indent=2, sort_keys=True
            )
            + "\n"
        )
        new_manifest_bytes = new_manifest_str.encode("utf-8")

        # Write the output archive
        with zipfile.ZipFile(args.output, "w") as out_zip:
            # Write contents in sorted filename order to match standard behavior
            for name in sorted(out_contents.keys()):
                out_zip.writestr(out_zipinfos[name], out_contents[name])

            # Write manifest at the end
            manifest_info = zipfile.ZipInfo(MANIFEST_NAME)
            manifest_info.date_time = in_zip.getinfo(MANIFEST_NAME).date_time
            manifest_info.compress_type = zipfile.ZIP_STORED
            manifest_info.external_attr = 0o644 << 16
            out_zip.writestr(manifest_info, new_manifest_bytes)


if __name__ == "__main__":
    main()