x1blob 12548 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
XXGPlayKit 日志解析工具
支持解析加密和明文日志文件
支持 Mac 和 Windows 系统
"""

import os
import sys
import base64
import json
import argparse
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
from datetime import datetime
import re

class LogParser:
    def __init__(self):
        self.file_separator = "=== %@ ==="
        self.encryption_enabled = False

    def detect_encryption(self, content):
        """检测日志是否加密"""
        lines = content.strip().split('\n')
        base64_count = 0
        total_lines = 0

        for line in lines:
            line = line.strip()
            if not line or line.startswith("==="):
                continue

            total_lines += 1

            # 检查是否是base64格式
            try:
                # base64字符串只包含A-Z, a-z, 0-9, +, /, =
                if re.match(r'^[A-Za-z0-9+/]*={0,2}$', line) and len(line) % 4 == 0:
                    decoded = base64.b64decode(line)
                    if len(decoded) > 0:
                        base64_count += 1
            except:
                pass

        # 如果超过80%的行都是base64格式，认为是加密日志
        if total_lines > 0 and (base64_count / total_lines) > 0.8:
            return True
        return False



    def parse_log_content(self, content):
        """解析日志内容"""
        if not content.strip():
            return "日志文件为空"

        # 基本的Unicode内容清理
        content = self.clean_unicode_content(content)

        # 检测是否加密
        is_encrypted = self.detect_encryption(content)

        if is_encrypted:
            return self.parse_encrypted_log(content)
        else:
            return self.parse_plain_log(content)

    def clean_unicode_content(self, content):
        """基本的Unicode内容清理，主要用于命令行模式"""
        try:
            # 对于命令行模式，保持原始内容，只做基本的错误字符处理
            cleaned_content = ""
            for char in content:
                # 保留所有有效的Unicode字符，只替换无效字符
                try:
                    # 测试字符是否可以正常编码
                    char.encode('utf-8')
                    cleaned_content += char
                except UnicodeEncodeError:
                    # 用替换字符代替无效字符
                    cleaned_content += "�"
            return cleaned_content
        except Exception:
            # 如果处理失败，返回原内容
            return content

    def parse_encrypted_log(self, content):
        """解析加密日志"""
        lines = content.strip().split('\n')
        result = ["=== 解密日志结果 ===\n"]

        for line in lines:
            line = line.strip()
            if not line:
                continue

            # 检查是否是文件分隔符
            if line.startswith("===") and line.endswith("==="):
                result.append("\n" + line + "\n")
                continue

            # 解密base64内容
            try:
                # 1. Base64解码
                encrypted_data = base64.b64decode(line)

                # 2. 检查数据长度（至少需要16字节IV）
                if len(encrypted_data) < 16:
                    result.append("[数据长度不足] " + line)
                    continue

                # 3. 提取IV（前16字节）
                iv = encrypted_data[:16]

                # 4. 提取密文（从第17字节开始）
                cipher_data = encrypted_data[16:]

                # 5. XOR解密
                decrypted_bytes = bytearray()
                for i, cipher_byte in enumerate(cipher_data):
                    plain_byte = cipher_byte ^ iv[i % 16]
                    decrypted_bytes.append(plain_byte)

                # 6. 转换为字符串
                decrypted_line = decrypted_bytes.decode('utf-8', errors='ignore')
                result.append(decrypted_line)

            except Exception:
                # 如果解密失败，显示原始内容
                result.append("[解密失败] " + line)

        return '\n'.join(result)

    def parse_plain_log(self, content):
        """解析明文日志"""
        lines = content.strip().split('\n')
        result = ["=== 明文日志解析结果 ===\n"]

        for line in lines:
            line = line.strip()
            if not line:
                continue

            # 检查是否是文件分隔符
            if line.startswith("===") and line.endswith("==="):
                result.append("\n" + line + "\n")
                continue

            # 直接显示原始内容，不进行格式解析
            result.append(line)

        return '\n'.join(result)



class LogParserGUI:
    def __init__(self):
        self.parser = LogParser()
        self.setup_gui()

    def setup_gui(self):
        """设置GUI界面"""
        self.root = tk.Tk()
        self.root.title("XXGPlayKit 日志解析工具")
        self.root.geometry("1000x700")

        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # 配置网格权重
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(2, weight=1)

        # 文件选择
        ttk.Label(main_frame, text="选择日志文件:").grid(row=0, column=0, sticky=tk.W, pady=(0, 5))

        file_frame = ttk.Frame(main_frame)
        file_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=(0, 5))
        file_frame.columnconfigure(0, weight=1)

        self.file_path = tk.StringVar()
        ttk.Entry(file_frame, textvariable=self.file_path, state="readonly").grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
        ttk.Button(file_frame, text="浏览", command=self.browse_file).grid(row=0, column=1)

        # 解析按钮
        ttk.Button(main_frame, text="解析日志", command=self.parse_log).grid(row=1, column=0, columnspan=2, pady=10)

        # 结果显示
        ttk.Label(main_frame, text="解析结果:").grid(row=2, column=0, sticky=(tk.W, tk.N), pady=(0, 5))

        # 创建文本框和滚动条
        text_frame = ttk.Frame(main_frame)
        text_frame.grid(row=2, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 5))
        text_frame.columnconfigure(0, weight=1)
        text_frame.rowconfigure(0, weight=1)

        self.result_text = scrolledtext.ScrolledText(text_frame, wrap=tk.WORD, width=80, height=30)
        self.result_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # 导出按钮
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=3, column=0, columnspan=2, pady=10)

        ttk.Button(button_frame, text="导出解析结果", command=self.export_result).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Button(button_frame, text="清空结果", command=self.clear_result).pack(side=tk.LEFT)

    def browse_file(self):
        """浏览文件"""
        file_path = filedialog.askopenfilename(
            title="选择日志文件",
            filetypes=[
                ("文本文件", "*.txt"),
                ("日志文件", "*.log"),
                ("所有文件", "*.*")
            ]
        )
        if file_path:
            self.file_path.set(file_path)

    def parse_log(self):
        """解析日志"""
        file_path = self.file_path.get()
        if not file_path:
            messagebox.showerror("错误", "请先选择日志文件")
            return

        if not os.path.exists(file_path):
            messagebox.showerror("错误", "文件不存在")
            return

        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()

            result = self.parser.parse_log_content(content)

            # 在插入GUI之前再次清理Unicode字符，确保完全兼容Tcl
            safe_result = self.make_text_tcl_safe(result)

            self.result_text.delete(1.0, tk.END)
            self.result_text.insert(1.0, safe_result)

            messagebox.showinfo("成功", "日志解析完成")

        except Exception as e:
            messagebox.showerror("Error", "Parse failed: " + str(e))

    def make_text_tcl_safe(self, text):
        """确保文本完全兼容Tcl，保存Unicode码点以便导出时恢复"""
        try:
            safe_text = ""
            for char in text:
                char_code = ord(char)
                # 只保留基本多语言平面内的字符
                if char_code <= 0xFFFF:
                    safe_text += char
                else:
                    # 保存Unicode码点，格式：[U+码点]
                    safe_text += "[U+" + format(char_code, 'X') + "]"
            return safe_text
        except Exception:
            # 如果处理失败，使用最保守的方法
            result = ""
            for char in text:
                char_code = ord(char)
                if char_code <= 0xFFFF:
                    result += char
                else:
                    result += "[U+" + format(char_code, 'X') + "]"
            return result

    def restore_unicode_from_export(self, text):
        """从导出文本中恢复Unicode字符"""
        import re
        try:
            # 匹配 [U+码点] 格式并替换为原始字符
            def replace_unicode(match):
                hex_code = match.group(1)
                try:
                    char_code = int(hex_code, 16)
                    return chr(char_code)
                except (ValueError, OverflowError):
                    return match.group(0)  # 如果转换失败，保持原样

            # 使用正则表达式替换所有 [U+XXXX] 格式
            restored_text = re.sub(r'\[U\+([0-9A-F]+)\]', replace_unicode, text)
            return restored_text
        except Exception:
            return text

    def export_result(self):
        """导出解析结果"""
        content = self.result_text.get(1.0, tk.END).strip()
        if not content:
            messagebox.showwarning("警告", "没有可导出的内容")
            return

        file_path = filedialog.asksaveasfilename(
            title="保存解析结果",
            defaultextension=".txt",
            filetypes=[
                ("文本文件", "*.txt"),
                ("所有文件", "*.*")
            ]
        )

        if file_path:
            try:
                # 恢复Unicode字符
                restored_content = self.restore_unicode_from_export(content)

                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(restored_content)
                messagebox.showinfo("成功", "解析结果已导出\n(已恢复原始emoji字符)")
            except Exception as e:
                messagebox.showerror("Error", "Export failed: " + str(e))

    def clear_result(self):
        """清空结果"""
        self.result_text.delete(1.0, tk.END)

    def run(self):
        """运行GUI"""
        self.root.mainloop()

def main():
    """主函数"""
    parser = argparse.ArgumentParser(description="XXGPlayKit 日志解析工具")
    parser.add_argument("-f", "--file", help="要解析的日志文件路径")
    parser.add_argument("-o", "--output", help="输出文件路径")
    parser.add_argument("--gui", action="store_true", help="启动图形界面")

    args = parser.parse_args()

    if args.gui or (not args.file):
        # 启动GUI
        app = LogParserGUI()
        app.run()
    else:
        # 命令行模式
        log_parser = LogParser()

        if not os.path.exists(args.file):
            print("Error: File " + args.file + " does not exist")
            sys.exit(1)

        try:
            with open(args.file, 'r', encoding='utf-8') as f:
                content = f.read()

            result = log_parser.parse_log_content(content)

            if args.output:
                with open(args.output, 'w', encoding='utf-8') as f:
                    f.write(result)
                print("Parse result saved to: " + args.output)
            else:
                print(result)

        except Exception as e:
            print("Parse failed: " + str(e))
            sys.exit(1)

if __name__ == "__main__":
    main()
£