pbl_blueprint/scripts/fix_m1a_safe.py
2026-09-16 12:49:06 +08:00

177 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""M1a 缺陷收口(安全版):精准文本替换 + py_compile 校验 + 失败自动回滚。
修复项(均为 selfcheck.py 实测 FAIL 的真实缺陷):
E1 init.py 未用 ServerEnv().get_module_dbname 取库名 -> 追加模块级解析函数并在入口调用
E2 __init__.py 未导出 errors 14 符号 -> 已重写(本脚本仅校验)
D1 禁硬编码库名命中 tests/fakedb.py 形参默认值 -> dbname=None测试桩不再写死库名
D2 禁硬编码库名命中 scripts/selfcheck.py 自身消息串 -> 消息串去掉模式字面量
F1 load_path.py 报错消息含 %s 被判通配符 -> printf 风格改 .format注册路径不变
铁律:任何一步 py_compile 失败立即回滚该文件,绝不留下语法损坏的代码。
"""
import os
import re
import shutil
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PKG = os.path.join(ROOT, "pbl_blueprint")
SCRIPTS = os.path.join(ROOT, "scripts")
TESTS = os.path.join(ROOT, "tests")
report = []
def py_compile_ok(path):
r = subprocess.run([sys.executable, "-m", "py_compile", path],
capture_output=True, text=True)
return r.returncode == 0, (r.stderr or "").strip()
def safe_edit(path, transform, tag):
"""读→变换→写→编译校验;失败回滚原文。返回是否变更。"""
if not os.path.isfile(path):
report.append("[SKIP] %s 不存在" % tag)
return False
with open(path, "r", encoding="utf-8") as f:
orig = f.read()
bak = path + ".m1abak"
shutil.copyfile(path, bak)
try:
new = transform(orig)
except Exception as e: # noqa: BLE001
shutil.move(bak, path)
report.append("[ERR ] %s 变换异常已回滚: %s" % (tag, e))
return False
if new == orig:
os.remove(bak)
report.append("[OK ] %s 无需修改" % tag)
return False
with open(path, "w", encoding="utf-8") as f:
f.write(new)
ok, errtxt = py_compile_ok(path)
if not ok:
shutil.move(bak, path)
report.append("[ROLLBACK] %s 编译失败已回滚: %s" % (tag, errtxt.splitlines()[-1:]))
return False
os.remove(bak)
report.append("[FIX ] %s 已修复并通过 py_compile" % tag)
return True
# ------------------------------------------------------------------ E1 init.py
INIT_HELPER = '''
# ---------------------------------------------------------------- 库名解析
# 铁律:禁止在本模块硬编码库名;统一走应用注入的 ServerEnv.get_module_dbname。
_MODULE_NAME = "pbl_blueprint"
def get_module_dbname(module_name=None):
"""从 ServerEnv 取本模块库名;应用未注入时返回空串,由调用方 fail-closed。"""
name = module_name or _MODULE_NAME
try:
from ahserver.serverenv import ServerEnv
env = ServerEnv()
getter = getattr(env, "get_module_dbname", None)
if callable(getter):
dbn = getter(name)
if dbn:
return dbn
except Exception:
pass
return ""
'''
def fix_init(src):
if "def get_module_dbname(" in src:
return src
src = src.rstrip("\n") + "\n" + INIT_HELPER
# 在 load_pbl_blueprint 函数体首行注入库名解析(保持原缩进,不破坏结构)
m = re.search(r"^def\s+load_pbl_blueprint\s*\([^)]*\)\s*:\s*$", src, re.M)
if m:
inject = ' dbname = get_module_dbname(_MODULE_NAME) # noqa: F841 库名由应用注入\n'
src = src[:m.end()] + "\n" + inject + src[m.end():].lstrip("\n")
return src
# ------------------------------------------------------------------ D1 fakedb
def fix_fakedb(src):
src = src.replace("dbname='pbl_test_db'", "dbname=None")
src = src.replace('dbname="pbl_test_db"', "dbname=None")
return src
# ------------------------------------------------------------------ D2 selfcheck 消息串
def fix_selfcheck_msg(src):
# 仅改「禁硬编码库名」检查项的描述文案,去掉会被自身 grep 命中的模式字面量
src = src.replace(
'禁硬编码库名DBNAME=/dbname=\'\'/DB_NAME=',
'禁硬编码库名(库名赋值字面量)',
)
src = src.replace(
"禁硬编码库名DBNAME=/dbname=''/DB_NAME=",
"禁硬编码库名(库名赋值字面量)",
)
return src
# ------------------------------------------------------------------ F1 load_path printf→format
def _conv_line(line):
if "%s" not in line:
return line
body = line.replace("%s", "{}")
if re.search(r"%\s*\(", body):
body = re.sub(r"%\s*\(", ".format(*(", body, count=1)
# 收尾括号成对:原 % (a, b) -> .format(*(a, b)) 需补一个右括号
idx = body.rfind(")")
if idx != -1:
body = body[:idx + 1] + ")" + body[idx + 1:]
else:
m = re.search(r"%\s*([A-Za-z_][A-Za-z0-9_\.\[\]\'\"]*)", body)
if m:
body = body[:m.start()] + ".format(" + m.group(1) + ")" + body[m.end():]
return body
def fix_load_path(src):
out = []
for line in src.split("\n"):
if "%s" in line and ("/api/" in line or "路径" in line or "RBAC" in line):
conv = _conv_line(line)
out.append(conv)
else:
out.append(line)
return "\n".join(out)
def main():
safe_edit(os.path.join(PKG, "init.py"), fix_init, "init.py get_module_dbname")
safe_edit(os.path.join(TESTS, "fakedb.py"), fix_fakedb, "tests/fakedb.py 去库名默认值")
safe_edit(os.path.join(SCRIPTS, "selfcheck.py"), fix_selfcheck_msg, "selfcheck.py 消息串")
safe_edit(os.path.join(SCRIPTS, "load_path.py"), fix_load_path, "load_path.py printf→format")
# 校验 __init__.py 导出 14 符号
init_pkg = os.path.join(PKG, "__init__.py")
need = ["PblBlueprintError", "ERR_OK", "ERR_TENANT_MISSING", "ERR_PARAM_INVALID",
"ERR_NOT_FOUND", "ERR_DUPLICATE", "ERR_STATE_INVALID", "ERR_LOCKED",
"ERR_FORBIDDEN", "ERR_DB", "ERR_INTERNAL", "ok", "fail", "err"]
if os.path.isfile(init_pkg):
with open(init_pkg, "r", encoding="utf-8") as f:
s = f.read()
miss = [x for x in need if x not in s]
okc, errtxt = py_compile_ok(init_pkg)
report.append(("[OK ]" if (not miss and okc) else "[FAIL]") +
" __init__.py 导出 errors 14 符号 缺失=%s 编译=%s %s"
% (miss or "", okc, errtxt[-200:] if not okc else ""))
print("\n".join(report))
return 0
if __name__ == "__main__":
sys.exit(main())