#!/usr/bin/env python3
"""
Extract serialized FileDescriptorProto bytes from jadx-generated *Proto.java
files and regenerate clean .proto definitions using the real protobuf library.
"""
import re
import sys
import os
from google.protobuf import descriptor_pb2

# proto3 -> no "optional" label; proto2 prints labels.
TYPE_MAP = {
    1: 'double', 2: 'float', 3: 'int64', 4: 'uint64', 5: 'int32',
    6: 'fixed64', 7: 'fixed32', 8: 'bool', 9: 'string', 11: 'message',
    12: 'bytes', 13: 'uint32', 14: 'enum', 15: 'sfixed32', 16: 'sfixed64',
    17: 'sint32', 18: 'sint64',
}

def field_type_str(f):
    if f.type_name:
        return f.type_name.lstrip('.')
    return TYPE_MAP.get(f.type, f'scalar{f.type}')

def render_field(f, syntax):
    ty = field_type_str(f)
    label = ''
    if f.label == descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED:
        if f.options.packed:
            label = 'packed repeated '
        else:
            label = 'repeated '
    elif syntax == 'proto2' and f.label == descriptor_pb2.FieldDescriptorProto.LABEL_REQUIRED:
        label = 'required '
    elif syntax == 'proto2':
        label = 'optional '
    return f'  {label}{ty} {f.name} = {f.number};'

def render_enum(e, indent):
    lines = [f'{indent}enum {e.name} {{']
    for v in e.value:
        lines.append(f'{indent}  {v.name} = {v.number};')
    lines.append(f'{indent}}}')
    return '\n'.join(lines)

def render_message(m, indent, syntax):
    lines = [f'{indent}message {m.name} {{']
    for oneof in m.oneof_decl:
        lines.append(f'{indent}  // oneof {oneof.name}')
    for nested in m.enum_type:
        lines.append(render_enum(nested, indent + '  '))
    for nested in m.nested_type:
        lines.append(render_message(nested, indent + '  ', syntax))
    for f in m.field:
        lines.append(render_field(f, syntax))
    lines.append(f'{indent}}}')
    return '\n'.join(lines)

def render_file(fd):
    out = [f'// source: {fd.name}']
    if fd.package:
        out.append(f'package {fd.package};')
    syntax = fd.syntax or 'proto3'
    out.append(f'syntax = "{syntax}";')
    if fd.dependency:
        out.append('')
        for dep in fd.dependency:
            out.append(f'import "{dep}";')
    out.append('')
    for e in fd.enum_type:
        out.append(render_enum(e, ''))
        out.append('')
    for m in fd.message_type:
        out.append(render_message(m, '', syntax))
        out.append('')
    return '\n'.join(out)

DESC_RE = re.compile(
    r'internalBuildGeneratedFileFrom\(new String\[\]\{(.*?)\}\s*,\s*new Descriptors',
    re.DOTALL)

def decode_java_string_literal_body(lit):
    """Decode a Java string-literal body (between quotes) to raw bytes."""
    out = bytearray()
    i = 0; n = len(lit)
    while i < n:
        c = lit[i]
        if c == '\\':
            nxt = lit[i+1]
            simple = {'n': 10, 'r': 13, 't': 9, '"': 34, "'": 39, '\\': 92, '0': 0, 'b': 8, 'f': 12}
            if nxt in simple:
                out.append(simple[nxt]); i += 2
            elif nxt == 'u':
                out += chr(int(lit[i+2:i+6], 16)).encode('latin-1', 'replace')
                i += 6
            else:
                out.append(ord(nxt) & 0xff); i += 2
        else:
            out += c.encode('latin-1', 'replace'); i += 1
    return bytes(out)

def extract_descriptor_bytes(java_path):
    txt = open(java_path, encoding='utf-8', errors='replace').read()
    m = DESC_RE.search(txt)
    if not m:
        return None
    body = m.group(1)
    lits = re.findall(r'"((?:[^"\\]|\\.)*)"', body)
    # protobuf concatenates ALL adjacent string elements into ONE descriptor
    combined = b''.join(decode_java_string_literal_body(l) for l in lits)
    return combined if combined else None

def main():
    proto_dir = sys.argv[1]
    out_dir = sys.argv[2]
    os.makedirs(out_dir, exist_ok=True)
    total_msgs = 0
    for fn in sorted(os.listdir(proto_dir)):
        if not fn.endswith('Proto.java'):
            continue
        path = os.path.join(proto_dir, fn)
        raw = extract_descriptor_bytes(path)
        base = fn[:-5].replace('Proto', '')
        if not raw:
            print(f'  [WARN] no descriptor in {fn}')
            continue
        try:
            fd_set = descriptor_pb2.FileDescriptorSet()
            fd = fd_set.file.add()
            fd.MergeFromString(raw)
        except Exception as ex:
            # may be a FileDescriptorSet directly, or multiple
            try:
                fd_set.ParseFromString(raw)
                if not fd_set.file:
                    raise
            except Exception as ex2:
                print(f'  [ERR ] {fn}: {ex2}')
                continue
        out_path = os.path.join(out_dir, base + '.proto')
        chunks = []
        for one_fd in fd_set.file:
            chunks.append(render_file(one_fd))
            total_msgs += len(one_fd.message_type)
        with open(out_path, 'w', encoding='utf-8') as f:
            f.write('\n\n'.join(chunks) + '\n')
        print(f'  [OK] {fn} -> {base}.proto ({len(raw)} bytes, {len(fd_set.file)} file(s), {sum(len(x.message_type) for x in fd_set.file)} msgs)')
    print(f'\nTotal messages recovered: {total_msgs}')

if __name__ == '__main__':
    main()
