惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
MongoDB | Blog
MongoDB | Blog
腾讯CDC
V
V2EX
Martin Fowler
Martin Fowler
A
About on SuperTechFans
大猫的无限游戏
大猫的无限游戏
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Check Point Blog
博客园 - 【当耐特】
Cisco Talos Blog
Cisco Talos Blog
The Hacker News
The Hacker News
K
Kaspersky official blog
Security Latest
Security Latest
H
Help Net Security
博客园_首页
美团技术团队
Spread Privacy
Spread Privacy
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
S
SegmentFault 最新的问题
G
Google Developers Blog
NISL@THU
NISL@THU
爱范儿
爱范儿
I
Intezer
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic
P
Privacy International News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Security @ Cisco Blogs
Schneier on Security
Schneier on Security
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
V
Vulnerabilities – Threatpost
W
WeLiveSecurity
P
Palo Alto Networks Blog
G
GRAHAM CLULEY
Hacker News: Ask HN
Hacker News: Ask HN
I
InfoQ
The Cloudflare Blog
F
Full Disclosure
SecWiki News
SecWiki News
宝玉的分享
宝玉的分享
N
Netflix TechBlog - Medium

博客园 - GKLBB

软件神器 --- ctf靶场 之 pwn.college 术语俗话 --- 什么是加密狗 软件神器 --- 自动电脑锁屏软件 软件研发 --- 网络安全 之 putty生成无密码登录密钥 软件神器 --- 视频剪辑 之 Avidemux MP4Joiner Shotcut 软件神器 --- 视频格式转化 之 handbrake 软件研发 --- 经验之谈 这个是什么图标,经常看到 软件研发 --- 术语俗话 之 多模态 术语俗话 --- 什么是大数据开发 软件研发 --- 术语俗话 之 什么是边缘计算 软件研发 --- 应知应会 之 什么是云计算开发 应知应会 --- 如何不装软件查看网速 常见问题解决 --- 360拦截接触 常见问题解决 --- 加入会议摄像头无法打开 网络安全 --- CTF打靶 之 ZIP伪加密 → Robot36慢扫电视 → 二维码补齐 → DeepSound隐写 → GPG解密 → Base64图片 → SilentEye隐写 软件研发 --- 应知应会 之 无错误的闪退如何解决 软件研发 --- 应知应会 之 为什么别人的软件如此复杂我的如此简单 网络安全 --- 应知应会 之 什么是旁站 应知应会 --- 如何查询备案 软件神器 --- 格式转化 之 jpg 2 svg 网络安全 --- CTF打靶 之 flag搜索 软件神器 --- 互联网测速 之 软件神器 --- 局域网测速 之 iperf 常见问题解决 --- 模拟器USG6000打开后,启动设备失败,错误40 软件研发 --- AI网络 之 ensp配置生成 软件研发 --- AI编程 之 极简流程 软件研发 --- AI生图产品比较 常见问题解答 --- 为什么我的服务一直被人DDOS 软件研发 --- 开发万能格式转化工具 网站神器 --- 在线格式转化 之 文档转md 术语俗话 --- 什么是云电脑 常见问题解决 --- 如何在没有安装杀毒软件的情况下知道这个文件是不是牧马 常见问题解决 --- Win10下VMware 17安装VMware Tools选项灰色/失效解决办法 常见问题解决 --- 云电脑报错 :PAGE FAULT IN NONPAGED AREA 记一次牧马的发现 人生感悟 --- 为什么我们生活中同一件事有多个品牌,他们到底有什么区别 软件运维 --- 云电脑安装打印机 常见问题解决 --- 网络打印默认端口 常见问题解决 --- 京瓷打印云电脑无法设置双面打印 软件研发 --- 网络安全 之 brupsuite如何设置上游袋里 术语俗话 --- 什么是同步和异步 我的目标 软件研发 --- dify配置 之 markword转word带有简单样式的方法 软件研发 --- 接口文档格式 常见问题 --- dify 的 deepseek是旧模型版本 OWASP 非营利性开源社区 计算机标准化组织 术语俗话 --- POSIX 就是 Unix 世界的"普通话",大家说同一种语言就能互相理解。 我的数字生活唯一选择 软件开发 --- 安卓开发 之 命名空间隔离 术语俗话 --- 页面对齐、 跨页 计算机的哪些人 --- 社交之王 马克·扎克伯格 计算机的哪些人 --- 重新定义新能源 之 埃隆·马斯克 软件研发 --- AI生成ppt word excel pdf的开发关键 软件研发 --- AI应用开发 之 AI生成Word 人生感悟 --- 致可悲的人 软件研发 --- pdf 表格图片 转 excel 常见问题解决 --- 装有系统的固态硬盘错误 人生感悟 --- 什么是行业黑话 术语俗话 --- Kubernetes 术语俗话 --- 什么是DBI,和hook什么区别 答疑解惑 --- 为什么苹果手机电池不允许拆卸 代码可视化技术 术语俗话 --- Wrap 函数 人生感悟 --- 逆向工程为什么十分枯燥且无聊 --- 这里引用一句逆向工程核心原理的话 人生感悟 --- 为什么干活时不见同事,活干完了就来了 软件研发 --- AI应用研发 之 提炼提示词 软件研发 --- AI应用开发 之 AI生成PPT最佳方案 基于屏幕-摄像头的单向数据传输方案设计 俗语俗话 --- 纯虚函数 硬件研发 --- 产品介绍 之 AX6 硬件研发 --- 产品大全 之 SUMAVISION CM50X 应知应会 --- 为什么U盘不识别但是还是可以数据恢复回来 应知应会 --- 为什么我删除了一个文件但是还是可以恢复回来 软件研发 --- AI UI设计 之 PC端效果比对 常见问题修复 --- chrome浏览器白屏 应用安全 --- 逆向工程 之 C++类的本质 软件研发 --- AI提示词开发 之 代码注释提示词 dobby反编译 https://www.cnblogs.com/Un1corn/p/18615567 应知应会 --- 手机作为热点,windows电脑临时作为网关,给其他电脑上网 应知应会 --- 大量小文件如何快速迁移 常见问题解决 --- apk安装过程中闪退 应用安全 --- 安卓逆向 之 dobby框架 在线生成 APK 图标工具推荐 一个开源库 术语俗话 --- 什么是软件即服务 软件神器 --- 常用谷歌插件 应用安全 --- 逆向技巧 之 识别未知函数 人生感悟 --- 为什么两句话可以说明白的事情要500字说明 软件研发 --- Dify 生成 PPT 方案分析 应用安全 --- 逆向技巧 之 IDA未知函数如何识别 https://yuuki.cool/ 应用安全 --- 逆向技巧 之 ELF节(Section) 与 段(Segment) 硬件研发 --- 接口 人生感悟 --- 为什么ld一直爱开会 应用安全 --- 逆向技巧 之 IDA和claude反编译缺陷 应用安全 --- 逆向技巧 之 ida反编译yahfa的so和源码的差异化比对
应用安全 --- IDAPro错误修复脚本 之 Rust format字符串识别
GKLBB · 2026-06-01 · via 博客园 - GKLBB
"""
rust_fmt_fixer.py  v6.0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
修复 v5.0 的两个核心 Bug:

Bug1: total_size 越界 —— 把相邻独立字符串也纳入范围
  原因: _compute_total_size 的扫描逻辑贪婪地读取后续可打印字节
  修复: 严格限制 total_size = Decoder 解析停止位置,
        不再向后扩展,不包含末尾 \0

Bug2: make_array 覆盖范围超出 fmt 模板
  原因: IDA make_array(ea, n) 中 n 是元素数,
        total_size 错误导致 n 过大
  修复: 精确传入 total_size,并在定义前验证边界

设计原则:
  - fmt 模板字节数组: [base_ea, base_ea + total_size)
  - 末尾 \0 (tag=0 的空字符串) 包含在内
  - 不触碰 base_ea + total_size 之后的任何字节
"""

import re
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Set, Tuple

import idc
import idaapi
import idautils
import ida_bytes
import ida_ida
import ida_nalt


# ═══════════════════════════════════════════════════════════════
#  §1  配置
# ═══════════════════════════════════════════════════════════════

class Cfg:
    verbose      : bool  = True
    dry_run      : bool  = False
    min_str_len  : int   = 1
    max_str_len  : int   = 8192
    min_printable: float = 0.55
    lea_scan_back: int   = 64
    max_pieces   : int   = 64
    add_comments : bool  = True
    auto_rename  : bool  = True
    max_cmt_len  : int   = 512

    AUTO_PREFIXES: Tuple[str, ...] = (
        "stru_", "byte_", "unk_", "word_",
        "dword_", "qword_", "off_", "asc_", "sub_", "loc_",
    )
    FMT_NEW_SIGS: Tuple[str, ...] = (
        "_ZN4core3fmt9Arguments3new",
        "_ZN4core3fmt9Arguments16new_v1",
        "fmt::Arguments::new",
        "core::fmt::Arguments::new",
        "Arguments3new",
    )


# ═══════════════════════════════════════════════════════════════
#  §2  日志
# ═══════════════════════════════════════════════════════════════

class Log:
    @staticmethod
    def _pr(tag: str, msg: str) -> None:
        print(f"[RustFmt][{tag}] {msg}")

    @classmethod
    def i(cls, m: str)  -> None: cls._pr("INFO ", m)
    @classmethod
    def w(cls, m: str)  -> None: cls._pr("WARN ", m)
    @classmethod
    def e(cls, m: str)  -> None: cls._pr("ERROR", m)
    @classmethod
    def ok(cls, m: str) -> None: cls._pr("OK   ", m)
    @classmethod
    def d(cls, m: str)  -> None:
        if Cfg.verbose:
            cls._pr("DBG  ", m)

    @staticmethod
    def sec(title: str) -> None:
        bar = "" * 58
        print(f"\n┌{bar}┐")
        print(f"│  {title:<56}│")
        print(f"└{bar}┘")


# ═══════════════════════════════════════════════════════════════
#  §3  IDA 工具层
# ═══════════════════════════════════════════════════════════════

class IDA:

    @staticmethod
    def is_64bit() -> bool:
        try:
            if hasattr(ida_ida, "inf_is_64bit"):
                return ida_ida.inf_is_64bit()
            return idaapi.get_inf_structure().is_64bit()
        except Exception:
            return True

    PTR: int = 8

    @classmethod
    def read_ptr(cls, ea: int) -> Optional[int]:
        try:
            if not ida_bytes.is_loaded(ea):
                return None
            if cls.PTR == 8:
                return ida_bytes.get_qword(ea) & 0xFFFF_FFFF_FFFF_FFFF
            return ida_bytes.get_dword(ea) & 0xFFFF_FFFF
        except Exception:
            return None

    @staticmethod
    def read_bytes(ea: int, n: int) -> Optional[bytes]:
        try:
            if n <= 0 or n > 0x10_0000:
                return None
            if not ida_bytes.is_loaded(ea):
                return None
            return ida_bytes.get_bytes(ea, n)
        except Exception:
            return None

    @staticmethod
    def read_u8(ea: int) -> Optional[int]:
        try:
            if not ida_bytes.is_loaded(ea):
                return None
            return ida_bytes.get_byte(ea)
        except Exception:
            return None

    @staticmethod
    def is_loaded(ea: int) -> bool:
        if ea in (0, idc.BADADDR) or ea is None:
            return False
        try:
            return bool(ida_bytes.is_loaded(ea))
        except Exception:
            return False

    @staticmethod
    def seg_name(ea: int) -> str:
        return idc.get_segm_name(ea) or ""

    @classmethod
    def in_rodata(cls, ea: int) -> bool:
        n = cls.seg_name(ea).lower()
        return any(k in n for k in
                   (".rdata", ".rodata", ".data.rel.ro", "__const"))

    @staticmethod
    def get_name(ea: int) -> str:
        return idc.get_name(ea, idc.GN_VISIBLE) or ""

    @staticmethod
    def set_name(ea: int, name: str) -> bool:
        try:
            return bool(idc.set_name(
                ea, name,
                idc.SN_NOWARN | idc.SN_NOCHECK | idc.SN_FORCE,
            ))
        except Exception:
            return False

    @staticmethod
    def all_names() -> List[Tuple[int, str]]:
        out: List[Tuple[int, str]] = []
        try:
            for item in idautils.Names():
                if not (isinstance(item, (list, tuple)) and len(item) == 2):
                    continue
                a, b = item
                if isinstance(a, int) and isinstance(b, str):
                    out.append((a, b))
                elif isinstance(a, str) and isinstance(b, int):
                    out.append((b, a))
        except Exception as ex:
            Log.w(f"all_names: {ex}")
        return out

    @staticmethod
    def set_cmt(ea: int, cmt: str) -> None:
        try:
            idc.set_cmt(ea, cmt[:Cfg.max_cmt_len], 0)
        except Exception:
            pass

    @staticmethod
    def get_cmt(ea: int) -> str:
        try:
            return idc.get_cmt(ea, 0) or ""
        except Exception:
            return ""

    @staticmethod
    def append_cmt(ea: int, text: str) -> None:
        old = IDA.get_cmt(ea)
        if text in old:
            return
        IDA.set_cmt(ea, f"{old} | {text}" if old else text)

    # ── 数据定义(精确范围控制)────────────────────────

    @staticmethod
    def force_undef(ea: int, size: int) -> None:
        """
        仅清除 [ea, ea+size) 范围内的定义。
        使用 DELIT_SIMPLE 而非 DELIT_EXPAND,
        避免意外扩展到相邻数据项。
        """
        try:
            # DELIT_SIMPLE: 只删除 ea 处的项,不扩展
            ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, size)
        except Exception:
            pass

    @staticmethod
    def force_undef_exact(ea: int, size: int) -> None:
        """
        逐字节清除,确保精确边界控制。
        用于 fmt 模板块的清除,不影响 ea+size 之后的数据。
        """
        # 先尝试整体清除
        try:
            ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, size)
        except Exception:
            pass

        # 再逐字节确认清除(处理跨越大项目的情况)
        offset = 0
        while offset < size:
            cur_ea = ea + offset
            try:
                item_sz = max(1, ida_bytes.get_item_size(cur_ea))
                ida_bytes.del_items(cur_ea, ida_bytes.DELIT_SIMPLE, 1)
                offset += 1
            except Exception:
                offset += 1

    @staticmethod
    def make_byte_array(ea: int, size: int) -> bool:
        """
        将 [ea, ea+size) 定义为字节数组。

        !! 关键: size 就是字节数,不是元素数 !!
        IDA make_array(ea, count) 中 count = 元素个数
        对于 byte 类型,元素大小=1,所以 count = size
        """
        if size <= 0:
            return False
        try:
            # 清除旧定义(精确范围)
            ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, size)

            # 定义第一个字节
            if not idc.create_byte(ea):
                return False

            # 扩展为数组(count = 字节数)
            if size > 1:
                idc.make_array(ea, size)

            # 验证:检查实际定义的大小
            actual = ida_bytes.get_item_size(ea)
            if actual != size:
                Log.d(f"    make_byte_array: 请求{size}字节,实际{actual}字节")
                # 不报错,继续(IDA 有时会合并相邻项)

            return True
        except Exception as ex:
            Log.d(f"    make_byte_array 异常: {ex}")
            return False

    @staticmethod
    def make_qword(ea: int) -> bool:
        try:
            ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, 8)
            return bool(idc.create_qword(ea))
        except Exception:
            return False

    @staticmethod
    def make_string(ea: int, length: int) -> bool:
        try:
            ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, length + 1)
            return bool(ida_bytes.create_strlit(ea, length, ida_nalt.STRTYPE_C))
        except Exception:
            return False

    @staticmethod
    def get_flags(ea: int) -> int:
        try:
            return ida_bytes.get_full_flags(ea)
        except Exception:
            return 0

    @staticmethod
    def is_qword(ea: int) -> bool:
        return ida_bytes.is_qword(IDA.get_flags(ea))

    @staticmethod
    def is_strlit(ea: int) -> bool:
        return ida_bytes.is_strlit(IDA.get_flags(ea))

    @staticmethod
    def get_item_size(ea: int) -> int:
        try:
            return ida_bytes.get_item_size(ea)
        except Exception:
            return 1

    @staticmethod
    def has_xref(ea: int) -> bool:
        """检查 ea 处是否有数据交叉引用(即被外部引用)"""
        try:
            for _ in idautils.XrefsTo(ea):
                return True
        except Exception:
            pass
        return False

    @staticmethod
    def mnem(ea: int) -> str:
        return idc.print_insn_mnem(ea).lower()

    @staticmethod
    def operand(ea: int, n: int) -> str:
        return idc.print_operand(ea, n).lower()

    @staticmethod
    def op_type(ea: int, n: int) -> int:
        return idc.get_operand_type(ea, n)

    @staticmethod
    def op_val(ea: int, n: int) -> int:
        return idc.get_operand_value(ea, n)

    @staticmethod
    def prev_head(ea: int) -> int:
        return idc.prev_head(ea)

    @staticmethod
    def xrefs_to(ea: int) -> list:
        try:
            return list(idautils.XrefsTo(ea))
        except Exception:
            return []

    @staticmethod
    def xrefs_from(ea: int) -> list:
        try:
            return list(idautils.XrefsFrom(ea))
        except Exception:
            return []

    @staticmethod
    def refresh() -> None:
        try:
            idaapi.refresh_idaview_anyway()
        except Exception:
            pass


IDA.PTR = 8 if IDA.is_64bit() else 4


# ═══════════════════════════════════════════════════════════════
#  §4  字符串工具
# ═══════════════════════════════════════════════════════════════

def printable_ratio(data: bytes) -> float:
    if not data:
        return 1.0
    good = sum(1 for b in data if 0x20 <= b <= 0x7E or b in (9, 10, 13))
    return good / len(data)


def decode_utf8(ea: int, length: int) -> Optional[str]:
    if not (Cfg.min_str_len <= length <= Cfg.max_str_len):
        return None
    if not IDA.is_loaded(ea):
        return None
    raw = IDA.read_bytes(ea, length)
    if raw is None:
        return None
    try:
        text = raw.decode("utf-8", errors="replace")
        good = sum(1 for c in text if c.isprintable() or c in "\n\r\t")
        if len(text) > 0 and (good / len(text)) < Cfg.min_printable:
            return None
        return text
    except Exception:
        return None


def make_id(s: str, maxlen: int = 28) -> str:
    r = re.sub(r"[^a-zA-Z0-9]", "_", s[:maxlen])
    r = re.sub(r"_+", "_", r).strip("_").lower()
    return r or "fmt"


def is_auto_name(ea: int) -> bool:
    name = IDA.get_name(ea)
    return bool(name) and any(name.startswith(p) for p in Cfg.AUTO_PREFIXES)


# ═══════════════════════════════════════════════════════════════
#  §5  数据结构
# ═══════════════════════════════════════════════════════════════

@dataclass
class StrPiece:
    ea            : int
    length        : int
    content       : str
    is_placeholder: bool = False
    tag_ea        : int  = 0


@dataclass
class DecodeResult:
    fmt_name  : str
    pieces    : List[StrPiece]
    raw_ea    : int
    # !! 关键字段: 解码器严格记录的解析字节数
    # 不包含任何后续字节,精确等于 TLV 序列的长度
    total_size: int = 0
    extra     : Dict = field(default_factory=dict)

    @property
    def strings(self) -> List[str]:
        return [p.content for p in self.pieces if not p.is_placeholder]

    @property
    def human_readable(self) -> str:
        return "".join(
            "{}" if p.is_placeholder else p.content
            for p in self.pieces
        )

    @property
    def preview(self) -> str:
        hr = self.human_readable
        return hr[:80] if hr else "<empty>"


# ═══════════════════════════════════════════════════════════════
#  §6  解码器(精确 total_size)
# ═══════════════════════════════════════════════════════════════

class Decoder:

    # ── FORMAT_A ────────────────────────────────────────

    def try_format_a(self, ea: int) -> Optional[DecodeResult]:
        P = IDA.PTR
        pieces_ptr = IDA.read_ptr(ea)
        pieces_len = IDA.read_ptr(ea + P)

        if pieces_ptr is None or pieces_len is None:
            return None
        if not IDA.is_loaded(pieces_ptr):
            return None
        if not (0 < pieces_len <= Cfg.max_pieces):
            return None
        if not IDA.in_rodata(pieces_ptr):
            return None

        pieces: List[StrPiece] = []
        for i in range(pieces_len):
            slot    = pieces_ptr + i * P * 2
            str_ptr = IDA.read_ptr(slot)
            str_len = IDA.read_ptr(slot + P)
            if str_ptr is None or str_len is None:
                break
            if str_ptr == 0 and str_len == 0:
                pieces.append(StrPiece(0, 0, ""))
                continue
            if not IDA.is_loaded(str_ptr):
                break
            if str_len > Cfg.max_str_len:
                break
            content = decode_utf8(str_ptr, str_len)
            if content is None:
                break
            pieces.append(StrPiece(str_ptr, str_len, content))

        if not pieces:
            return None
        return DecodeResult("FORMAT_A", pieces, ea)

    # ── FORMAT_B ─────────────────────────────────────────

    def try_format_b(self, ea: int) -> Optional[DecodeResult]:
        """
        严格 TLV 解析,精确记录 total_size。

        终止条件(任意一个满足即停止):
          1. 遇到 tag=0 (空字符串,通常是序列结束标记)
          2. 连续解析失败次数 > 阈值
          3. 超过最大扫描字节数

        total_size = 解析停止时的 offset(从 ea 起的字节偏移)
        这个值精确界定了 fmt 模板的边界,不包含后续任何数据。
        """
        pieces          : List[StrPiece] = []
        offset          : int            = 0
        max_scan        : int            = 512   # 单个模板不超过 512 字节
        consecutive_fail: int            = 0
        MAX_FAIL        : int            = 3

        while offset < max_scan:
            tag_ea   = ea + offset
            tag_byte = IDA.read_u8(tag_ea)

            if tag_byte is None:
                break

            # ── tag=0: 序列结束标记 ──────────────────────
            # Rust fmt 模板以 tag=0 (空字符串) 结尾
            if tag_byte == 0:
                pieces.append(StrPiece(
                    ea=ea + offset + 1, length=0, content="",
                    tag_ea=tag_ea,
                ))
                offset += 1  # 包含这个结束字节
                break        # ← 严格在此终止,不继续向后

            # ── 普通字符串片段: tag < 0x80 ───────────────
            if (tag_byte & 0x80) == 0:
                length  = tag_byte
                str_ea  = ea + offset + 1

                content = decode_utf8(str_ea, length)
                if content is None:
                    consecutive_fail += 1
                    if consecutive_fail >= MAX_FAIL:
                        break
                    offset += 1
                    continue

                pieces.append(StrPiece(
                    ea=str_ea, length=length, content=content,
                    tag_ea=tag_ea,
                ))
                Log.d(f"    B str +{offset:#05x} len={length} '{content[:40]}'")
                offset += 1 + length
                consecutive_fail = 0

            # ── 占位符: tag & 0xC0 == 0xC0 ───────────────
            elif (tag_byte & 0xC0) == 0xC0:
                pieces.append(StrPiece(
                    ea=0, length=0, content="{}",
                    is_placeholder=True, tag_ea=tag_ea,
                ))
                Log.d(f"    B ph  +{offset:#05x} tag={tag_byte:#04x}")
                offset += 1
                consecutive_fail = 0

            # ── 其他高字节 ───────────────────────────────
            else:
                Log.d(f"    B ctl +{offset:#05x} {tag_byte:#04x}")
                consecutive_fail += 1
                if consecutive_fail >= MAX_FAIL:
                    break
                offset += 1

        real = [p for p in pieces if not p.is_placeholder and p.length > 0]
        if not real:
            return None

        result = DecodeResult("FORMAT_B", pieces, ea)
        # !! total_size 严格等于解析消耗的字节数
        # 这就是 fmt 模板的精确大小,后续数组定义不得超出此范围
        result.total_size = offset
        Log.d(f"    B total_size={offset} (0x{ea:X}..0x{ea+offset:X})")
        return result

    # ── FORMAT_C ─────────────────────────────────────────

    def try_format_c(self, ea: int) -> Optional[DecodeResult]:
        raw = IDA.read_bytes(ea, 256)
        if raw is None:
            return None

        pieces    : List[StrPiece] = []
        cur       : bytearray      = bytearray()
        cur_start : int            = 0

        for i, b in enumerate(raw):
            if 0x20 <= b <= 0x7E or b in (9, 10, 13):
                if not cur:
                    cur_start = i
                cur.append(b)
            else:
                if len(cur) >= 3:
                    try:
                        text = cur.decode("utf-8", errors="strict")
                        pieces.append(StrPiece(
                            ea=ea + cur_start, length=len(cur), content=text,
                        ))
                    except Exception:
                        pass
                cur = bytearray()

        if len(cur) >= 3:
            try:
                pieces.append(StrPiece(
                    ea=ea + cur_start, length=len(cur),
                    content=cur.decode("utf-8", errors="strict"),
                ))
            except Exception:
                pass

        if not pieces:
            return None
        return DecodeResult("FORMAT_C", pieces, ea)

    # ── 主入口 ───────────────────────────────────────────

    def decode(self, ea: int) -> Optional[DecodeResult]:
        Log.d(f"  decode 0x{ea:X}")
        for tag, method in (
            ("A", self.try_format_a),
            ("B", self.try_format_b),
            ("C", self.try_format_c),
        ):
            r = method(ea)
            if r:
                Log.d(f"  ✓ {tag}: {r.preview}")
                return r
            Log.d(f"  ✗ {tag}")
        return None


# ═══════════════════════════════════════════════════════════════
#  §7  调用点追踪
# ═══════════════════════════════════════════════════════════════

_ARG0_REGS = ("rcx", "ecx", "rdi", "edi")


@dataclass
class CallSite:
    call_ea    : int
    func_ea    : int
    template_ea: int
    result     : Optional[DecodeResult]

    @property
    def strings(self) -> List[str]:
        return self.result.strings if self.result else []

    @property
    def preview(self) -> str:
        return self.result.preview if self.result else "<no decode>"


class CallTracer:

    def __init__(self):
        self._decoder = Decoder()

    def trace(self, call_ea: int) -> Optional[CallSite]:
        func_ea = idc.get_func_attr(call_ea, idc.FUNCATTR_START)
        tmpl_ea = self._find_arg0(call_ea)
        if tmpl_ea is None:
            return None
        result = self._decoder.decode(tmpl_ea)
        return CallSite(
            call_ea=call_ea, func_ea=func_ea,
            template_ea=tmpl_ea, result=result,
        )

    def _find_arg0(self, call_ea: int) -> Optional[int]:
        cur     = call_ea
        reg_map : Dict[str, int] = {}

        for _ in range(Cfg.lea_scan_back):
            cur = IDA.prev_head(cur)
            if cur == idc.BADADDR:
                break

            m   = IDA.mnem(cur)
            op0 = IDA.operand(cur, 0)

            if m == "lea":
                addr = self._mem_op_ea(cur, 1)
                if addr is not None and IDA.is_loaded(addr):
                    reg_map[op0] = addr
                    if op0 in _ARG0_REGS:
                        return addr

            elif m == "mov" and op0 in _ARG0_REGS:
                op1 = IDA.operand(cur, 1).strip()
                if op1 in reg_map:
                    return reg_map[op1]
                addr = self._imm_op_ea(cur, 1)
                if addr is not None and IDA.is_loaded(addr):
                    return addr

            for xref in IDA.xrefs_from(cur):
                xtype = getattr(xref, "type", -1)
                if xtype in (1, 2, 3, 4, 16, 17, 18, 19, 20):
                    ref_ea = xref.to
                    if IDA.is_loaded(ref_ea) and IDA.in_rodata(ref_ea):
                        reg_map[op0] = ref_ea
                        if op0 in _ARG0_REGS:
                            return ref_ea

        return None

    @staticmethod
    def _mem_op_ea(insn_ea: int, op_idx: int) -> Optional[int]:
        ot = IDA.op_type(insn_ea, op_idx)
        ov = IDA.op_val(insn_ea, op_idx)
        if ot in (idc.o_mem, idc.o_displ):
            return ov
        if ot == idc.o_imm and IDA.is_loaded(ov):
            return ov
        return None

    @staticmethod
    def _imm_op_ea(insn_ea: int, op_idx: int) -> Optional[int]:
        ot = IDA.op_type(insn_ea, op_idx)
        ov = IDA.op_val(insn_ea, op_idx)
        if ot in (idc.o_imm, idc.o_mem) and IDA.is_loaded(ov):
            return ov
        return None


# ═══════════════════════════════════════════════════════════════
#  §8  扫描器
# ═══════════════════════════════════════════════════════════════

class Scanner:

    def __init__(self):
        self._tracer = CallTracer()

    def find_fmt_functions(self) -> List[int]:
        found: Set[int] = set()
        for ea, name in IDA.all_names():
            for sig in Cfg.FMT_NEW_SIGS:
                if sig in name:
                    found.add(ea)
                    Log.i(f"  ✓ {name} @ 0x{ea:X}")
                    break
        return sorted(found)

    def scan(self) -> List[CallSite]:
        fmt_fns = self.find_fmt_functions()
        if not fmt_fns:
            Log.w("未找到 fmt::Arguments::new")
            return []

        sites      : List[CallSite] = []
        seen_calls : Set[int]       = set()

        for fn_ea in fmt_fns:
            refs = IDA.xrefs_to(fn_ea)
            Log.i(f"  函数 0x{fn_ea:X}: {len(refs)} 个引用")

            for xref in refs:
                call_ea = xref.frm
                if call_ea in seen_calls:
                    continue
                seen_calls.add(call_ea)

                if IDA.mnem(call_ea) not in ("call", "jmp", "bl", "blx"):
                    continue

                site = self._tracer.trace(call_ea)
                if site:
                    sites.append(site)

        return sites


# ═══════════════════════════════════════════════════════════════
#  §9  修复器
# ═══════════════════════════════════════════════════════════════

class Fixer:

    def __init__(self):
        self.n_sites   = 0
        self.n_fixed   = 0
        self.n_renamed = 0
        self.n_retyped = 0
        self.n_errors  = 0
        # 记录已处理的模板地址,防止重复修复破坏相邻数据
        self._done: Set[int] = set()

    # ── 公共入口 ─────────────────────────────────────────

    def fix_site(self, site: CallSite) -> bool:
        self.n_sites += 1
        try:
            return self._fix(site)
        except Exception as ex:
            Log.e(f"fix_site 0x{site.call_ea:X}: {ex}")
            self.n_errors += 1
            return False

    def fix_result(self, ea: int, result: DecodeResult) -> bool:
        try:
            return self._apply(ea, result, call_ea=None)
        except Exception as ex:
            Log.e(f"fix_result 0x{ea:X}: {ex}")
            return False

    # ── 内部实现 ─────────────────────────────────────────

    def _fix(self, site: CallSite) -> bool:
        if site.result is None:
            if Cfg.add_comments and not Cfg.dry_run:
                IDA.append_cmt(site.call_ea, "fmt::Arguments::new")
            return False
        ok = self._apply(site.template_ea, site.result, site.call_ea)
        if ok:
            self.n_fixed += 1
        return ok

    def _apply(self,
               data_ea : int,
               result  : DecodeResult,
               call_ea : Optional[int]) -> bool:

        # 同一模板地址只处理一次(可能被多个 call 引用)
        if data_ea in self._done:
            if call_ea and Cfg.add_comments and not Cfg.dry_run:
                IDA.append_cmt(call_ea, f'fmt!("{result.human_readable}")')
            return True
        self._done.add(data_ea)

        if Cfg.dry_run:
            Log.ok(f"[DRY] 0x{data_ea:X} [{result.fmt_name}]"
                   f" size={result.total_size}"
                   f' "{result.preview}"')
            return True

        # ① call 注释
        if call_ea and Cfg.add_comments:
            IDA.append_cmt(call_ea, f'fmt!("{result.human_readable}")')

        # ② 数据类型修复
        if result.fmt_name == "FORMAT_A":
            self._fix_format_a(data_ea, result)
        elif result.fmt_name == "FORMAT_B":
            self._fix_format_b(data_ea, result)

        # ③ 起始地址注释
        if Cfg.add_comments:
            IDA.set_cmt(
                data_ea,
                f'const core::fmt::Arguments\n'
                f'fmt!("{result.human_readable}")',
            )

        # ④ 重命名
        if result.strings and Cfg.auto_rename:
            safe = make_id(result.strings[0])
            if is_auto_name(data_ea):
                if IDA.set_name(data_ea, f"rust_fmt_{safe}"):
                    self.n_renamed += 1

        Log.ok(f'0x{data_ea:X} [{result.fmt_name}]'
               f' size={result.total_size}'
               f' "{result.preview}"')
        return True

    # ── FORMAT_A ────────────────────────────────────────

    def _fix_format_a(self, base_ea: int, result: DecodeResult) -> None:
        P = IDA.PTR
        pieces_ptr = IDA.read_ptr(base_ea)
        if pieces_ptr is None:
            return

        for i, piece in enumerate(result.pieces):
            if piece.length == 0:
                continue
            slot_ea = pieces_ptr + i * P * 2
            ida_bytes.del_items(slot_ea, ida_bytes.DELIT_SIMPLE, P * 2)
            IDA.make_qword(slot_ea)
            IDA.make_qword(slot_ea + P)
            IDA.set_cmt(slot_ea, f'&str[{i}]: "{piece.content[:40]}"')

            if IDA.is_loaded(piece.ea) and piece.length > 0:
                if not IDA.is_strlit(piece.ea):
                    if IDA.make_string(piece.ea, piece.length):
                        self.n_retyped += 1
                IDA.append_cmt(piece.ea, f'utf8: "{piece.content[:40]}"')
                if is_auto_name(piece.ea):
                    safe = make_id(piece.content)
                    if IDA.set_name(piece.ea, f"rust_str_{safe}"):
                        self.n_renamed += 1

    # ── FORMAT_B(核心修复)──────────────────────────────

    def _fix_format_b(self, base_ea: int, result: DecodeResult) -> None:
        """
        将 [base_ea, base_ea + total_size) 定义为整体字节数组。

        !! 安全保证 !!
          1. 仅操作 total_size 字节,不超出边界
          2. 操作前检查边界处是否有外部 xref 引用
             如果有,说明 total_size 计算有误,缩短到该处
          3. 使用 DELIT_SIMPLE 而非 DELIT_EXPAND
        """
        total = result.total_size
        if total <= 0:
            Log.w(f"    FORMAT_B: total_size=0 @ 0x{base_ea:X},跳过")
            return

        # 安全边界检查:确认 [base_ea, base_ea+total) 内没有外部引用
        # 外部引用意味着该地址是独立数据的起始点,不该被覆盖
        safe_total = self._safe_size(base_ea, total)
        if safe_total != total:
            Log.w(f"    边界收缩: {total} → {safe_total} "
                  f"(0x{base_ea+safe_total:X} 有外部引用)")
        total = safe_total

        if total <= 0:
            Log.w(f"    FORMAT_B: 安全大小=0,跳过 0x{base_ea:X}")
            return

        Log.d(f"    FORMAT_B: make_byte_array 0x{base_ea:X} [{total}]")

        # 清除旧定义(精确范围,不扩展)
        ida_bytes.del_items(base_ea, ida_bytes.DELIT_SIMPLE, total)

        # 定义为整体字节数组
        ok = self._create_byte_array(base_ea, total)
        if ok:
            self.n_retyped += 1
        else:
            Log.w(f"    make_byte_array 失败 @ 0x{base_ea:X}[{total}]")

    def _safe_size(self, base_ea: int, requested: int) -> int:
        """
        从 base_ea+1 开始,找到第一个有外部 xref 的地址。
        返回从 base_ea 到该地址的字节数(不超过 requested)。

        base_ea 本身有 xref 是正常的(这是 fmt 模板的入口引用)。
        """
        for offset in range(1, requested):
            check_ea = base_ea + offset
            if not IDA.is_loaded(check_ea):
                return offset

            # 检查是否有外部数据引用
            for xref in IDA.xrefs_to(check_ea):
                xtype = getattr(xref, "type", -1)
                # 数据引用类型: dr_O(1), dr_W(2), dr_R(3), dr_T(4)
                if xtype in (1, 2, 3, 4):
                    Log.d(f"      xref @ +{offset} (0x{check_ea:X})"
                          f" from 0x{xref.frm:X} type={xtype}")
                    return offset

        return requested

    @staticmethod
    def _create_byte_array(ea: int, size: int) -> bool:
        """
        创建字节数组的最终实现。

        为什么不用 make_array:
          idc.make_array(ea, count) 有时会将 count 解释为
          "创建 count 个与 ea 处相同类型的元素",
          行为依赖 IDA 版本,不稳定。

        更可靠的方式:
          使用 ida_bytes.create_data 直接创建指定大小的数据项。
        """
        try:
            # 方式1: create_data (最可靠)
            # FF_BYTE = 0x00000000 (字节类型标志)
            ok = ida_bytes.create_data(
                ea,
                ida_bytes.FF_BYTE,
                size,
                idc.BADADDR,
            )
            if ok:
                return True
        except Exception as ex:
            Log.d(f"    create_data: {ex}")

        try:
            # 方式2: create_byte + make_array
            if idc.create_byte(ea):
                if size > 1:
                    idc.make_array(ea, size)
                return True
        except Exception as ex:
            Log.d(f"    create_byte+make_array: {ex}")

        return False

    # ── 报告 ─────────────────────────────────────────────

    def report(self) -> None:
        Log.sec("执行报告")
        Log.i(f"  处理调用点数 : {self.n_sites}")
        Log.i(f"  成功修复数   : {self.n_fixed}")
        Log.i(f"  数据重定义数 : {self.n_retyped}")
        Log.i(f"  符号重命名数 : {self.n_renamed}")
        Log.i(f"  错误数       : {self.n_errors}")
        if self.n_errors == 0:
            Log.ok("全部完成")
        Log.i("提示: F5 刷新 | Edit→Undo 撤销")


# ═══════════════════════════════════════════════════════════════
#  §10  主引擎
# ═══════════════════════════════════════════════════════════════

class Engine:

    def __init__(self):
        self._scanner = Scanner()
        self._fixer   = Fixer()

    def run(self) -> None:
        Log.sec("Rust fmt! 修复工具 v6.0  精确边界控制")
        Log.i(f"IDA  : {idaapi.get_kernel_version()}")
        Log.i(f"架构 : {'64-bit' if IDA.is_64bit() else '32-bit'}")
        Log.i(f"DRY  : {Cfg.dry_run}")

        Log.sec("阶段1  扫描")
        sites = self._scanner.scan()
        Log.i(f"调用点: {len(sites)}")
        if not sites:
            Log.w("无调用点 | 手动: fix_call(0x...) / fix_data(0x...)")
            return

        Log.sec("阶段2  修复")
        total = len(sites)
        for idx, site in enumerate(sites, 1):
            self._fixer.fix_site(site)
            if idx % 50 == 0 or idx == total:
                Log.i(f"  {idx}/{total}")

        IDA.refresh()
        self._fixer.report()


# ═══════════════════════════════════════════════════════════════
#  §11  便捷 API
# ═══════════════════════════════════════════════════════════════

_tracer  = CallTracer()
_decoder = Decoder()
_fixer   = Fixer()


def fix_call(call_ea: int) -> None:
    """通过 call 指令地址修复"""
    Log.sec(f"fix_call 0x{call_ea:X}")
    site = _tracer.trace(call_ea)
    if site:
        _fixer.fix_site(site)
    else:
        Log.w("追踪失败,尝试 fix_data()")


def fix_data(data_ea: int) -> None:
    """直接修复数据地址"""
    Log.sec(f"fix_data 0x{data_ea:X}")
    result = _decoder.decode(data_ea)
    if result:
        _fixer.fix_result(data_ea, result)
    else:
        Log.w("解码失败,尝试 dump()")


def dump(ea: int, size: int = 96) -> None:
    """打印字节并解码(不修改 IDA)"""
    raw = IDA.read_bytes(ea, size)
    if raw is None:
        print(f"无法读取 0x{ea:X}")
        return

    print(f"\n━━ 0x{ea:X} ({size} bytes) ━━")
    for i in range(0, len(raw), 16):
        chunk = raw[i:i+16]
        hex_s = " ".join(f"{b:02X}" for b in chunk)
        asc_s = "".join(chr(b) if 0x20 <= b <= 0x7E else "·" for b in chunk)
        print(f"  +{i:04X}  {hex_s:<48}  {asc_s}")

    print("\n━━ 解码 ━━")
    old, Cfg.verbose = Cfg.verbose, True
    result = _decoder.decode(ea)
    Cfg.verbose = old

    if result:
        print(f"  格式      : {result.fmt_name}")
        print(f"  总大小    : {result.total_size} bytes  "
              f"(0x{ea:X} → 0x{ea+result.total_size:X})")
        print(f'  格式字符串: "{result.human_readable}"')
        print(f"  片段:")
        for i, p in enumerate(result.pieces):
            kind = "PH " if p.is_placeholder else "STR"
            print(f"    [{i:02d}] {kind} tag=0x{p.tag_ea:X}"
                  f" data=0x{p.ea:X} len={p.length}"
                  f" '{p.content[:40]}'")

        # 显示边界之后的数据(验证不越界)
        end_ea  = ea + result.total_size
        next_raw = IDA.read_bytes(end_ea, 16)
        if next_raw:
            hex_s = " ".join(f"{b:02X}" for b in next_raw)
            asc_s = "".join(chr(b) if 0x20 <= b <= 0x7E else "·"
                            for b in next_raw)
            print(f"\n  边界后 0x{end_ea:X}: {hex_s}  {asc_s}")
            print(f"  (此处数据应不被修改)")
    else:
        print("  解码失败")


def verify(ea: int) -> None:
    """验证修复结果:检查 ea 处的数据类型和相邻数据完整性"""
    result = _decoder.decode(ea)
    if result is None:
        print(f"0x{ea:X}: 解码失败")
        return

    total = result.total_size
    end   = ea + total
    size  = IDA.get_item_size(ea)

    print(f"\n━━ verify 0x{ea:X} ━━")
    print(f"  预期大小 : {total} bytes  → 0x{end:X}")
    print(f"  实际大小 : {size} bytes")
    print(f"  大小匹配 : {'✓' if size == total else '✗'}")

    # 检查边界后的数据是否完整
    print(f"\n  边界后 0x{end:X} 的数据:")
    for offset in range(0, 32, 8):
        chunk_ea = end + offset
        raw = IDA.read_bytes(chunk_ea, 8)
        if raw is None:
            break
        item_sz = IDA.get_item_size(chunk_ea)
        name    = IDA.get_name(chunk_ea)
        print(f"    0x{chunk_ea:X} [{item_sz:2d}B] {raw.hex()}"
              f"  {name or '(unnamed)'}")


def set_verbose(v: bool) -> None: Cfg.verbose = v
def set_dry_run(v: bool) -> None: Cfg.dry_run = v; Log.i(f"dry_run={v}")


if __name__ == "__main__":
    Engine().run()