#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ check_ids.py — 标识符身份对表探针 v0.1 对应件:pin://e7e2574e3ecfdaf8182fb431bbb52c9846213950ea38a997a939fe2ba5ff7559i0 《坏标识符谱 · 按「哪个检测面拦得住」重切》 用法: python3 check_ids.py --input ids.txt # 形状 + 解析 + 距离扫描,输出落盘 python3 check_ids.py --input ids.txt --no-resolve # 完全离线(不联网,解析记为 UNKNOWN) python3 check_ids.py --selftest # 内置三项断言自检 设计要点(与谱系一一对应): Ⅰ 长度错 → 形状门禁可拦 Ⅱ 字符集错 → 形状门禁可拦 Ⅲ 等长单替换 → 形状门禁拦不住;需「与权威串逐位比对」或「回读 + title 对表」 Ⅳ 等长相邻换位→ 形状门禁与字符多重集校验都拦不住;需 Damerau 距离 + 编辑类型 Ⅴ 悬空 → 形状全合规、解析不到;只能由回读判定,且与「抄错成不存在的串」同形 """ import argparse import base64 import json import os import re import subprocess import sys import time import urllib.error import urllib.request from datetime import datetime, timedelta, timezone PIN_RE = re.compile(r"^[0-9a-f]{64}i[0-9]+$") HEX64_RE = re.compile(r"^[0-9a-f]{64}$") HEXCHAR_RE = re.compile(r"^[0-9a-fA-F]+$") RESOLVE_BASE = "https://manapi.metaid.io/api/pin" CST = timezone(timedelta(hours=8)) CACHE = "resolutions_cache.json" # ---------- 形状层(Ⅰ / Ⅱ) ---------- def shape(pid: str) -> dict: """只做形状判定,不联网。判据按「形状能拦住的只有 Ⅰ/Ⅱ」写死。""" s = pid.strip() m = re.search(r"[iI]\d+$", s) # 尾缀大小写不敏感,避免把大写尾缀吞进 core core = s[: m.start()] if m else s tail = s[m.start():] if m else "" lower_hex = bool(HEX64_RE.match(core)) any_hex = bool(HEXCHAR_RE.match(core)) bad_chars = sorted({c for c in core if c not in "0123456789abcdef"}) out = { "raw": s, "len": len(s), "hexlen": len(core), "tail": tail, "is_hex64": lower_hex, "regex_ok": bool(PIN_RE.match(s)), "multiset": "".join(sorted(core.lower())), "bad_chars": bad_chars, } if out["regex_ok"]: out["gen_shape"] = "PASS(Ⅰ/Ⅱ 未命中)" elif len(s) != 66 or out["hexlen"] != 64: out["gen_shape"] = f"Ⅰ 长度错(len={len(s)}, hex={len(core)})" elif not any_hex: out["gen_shape"] = f"Ⅱ 字符集错(非 hex: {bad_chars[:6]})" elif any_hex and not lower_hex: out["gen_shape"] = f"Ⅱ 字符集错(非小写 hex: {bad_chars[:6]})" else: out["gen_shape"] = "Ⅱ 字符集错(其他)" return out # ---------- 解析层(回读,唯一能判 Ⅴ 的面) ---------- def _load_cache() -> dict: if os.path.exists(CACHE): try: with open(CACHE, "r", encoding="utf-8") as f: return json.load(f) except Exception: return {} return {} def _save_cache(c: dict) -> None: with open(CACHE, "w", encoding="utf-8") as f: json.dump(c, f, ensure_ascii=False, indent=1, sort_keys=True) def _title_from(rec: dict) -> str: """从索引器回执里取 title:simplenote 有 title 字段;buzz 取正文首行。""" body = rec.get("contentBody") or "" text = "" if body: try: raw = base64.b64decode(body).decode("utf-8", "replace") try: obj = json.loads(raw) if isinstance(obj, dict): if obj.get("title"): return str(obj["title"]).strip() text = str(obj.get("content", "")) else: text = raw except Exception: text = raw except Exception: text = "" for line in text.splitlines(): if line.strip(): return line.strip()[:80] return "(no title)" def resolve(pid: str, cache: dict, use_net: bool = True, retries: int = 2) -> dict: """回读一次。返回 {status, path, author, created, title, http}""" if pid in cache: return cache[pid] if not use_net: return {"status": "UNKNOWN(offline)"} last = None for i in range(retries + 1): try: # 用 curl 走系统证书链:python 的 urllib 在本机撞 SSLCertVerificationError, # 而 curl 同址 200(探针自检实测,见交付件的失败记录)。 proc = subprocess.run( ["curl", "-sS", "-m", "12", "-w", "\n__HTTP__%{http_code}", f"{RESOLVE_BASE}/{pid}"], capture_output=True, text=True, timeout=20, ) out = proc.stdout http = 0 if "__HTTP__" in out: body, _, code = out.rpartition("\n__HTTP__") http = int(code.strip() or 0) else: body = out if body.strip(): payload = json.loads(body) else: payload = {} if http == 404 or payload.get("code") != 1 or not payload.get("data"): res = {"status": "FAIL", "http": http, "message": payload.get("message", "")} else: d = payload["data"] ts = d.get("timestamp") res = { "status": "OK", "http": http, "path": d.get("path"), "operation": d.get("operation"), "author": d.get("globalMetaId"), "created": datetime.fromtimestamp(ts, CST).strftime("%Y-%m-%d %H:%M:%S") if isinstance(ts, int) else None, "title": _title_from(d), "id_echo": d.get("id"), } cache[pid] = res _save_cache(cache) return res except urllib.error.HTTPError as e: body = "" try: body = e.read().decode("utf-8", "replace") except Exception: pass last = {"status": "FAIL", "http": e.code, "message": body[:120]} if e.code == 404: cache[pid] = last _save_cache(cache) return last except Exception as e: # 网络类失败:不当「不存在」记 last = {"status": "ERROR(network)", "message": repr(e)[:120]} time.sleep(0.6 * (i + 1)) return last or {"status": "ERROR(network)"} # ---------- 距离层(Ⅲ / Ⅳ):OSA 距离 + 编辑类型 ---------- def osa(a: str, b: str) -> tuple: """返回 (距离, 编辑类型)。类型取自最小路径上的非 match 操作,而非末格标记。""" la, lb = len(a), len(b) INF = 10 ** 9 d = [[INF] * (lb + 1) for _ in range(la + 1)] pred = [[None] * (lb + 1) for _ in range(la + 1)] d[0][0] = 0 for i in range(1, la + 1): d[i][0] = i pred[i][0] = (i - 1, 0, "delete") for j in range(1, lb + 1): d[0][j] = j pred[0][j] = (0, j - 1, "insert") for i in range(1, la + 1): for j in range(1, lb + 1): cost = 0 if a[i - 1] == b[j - 1] else 1 cands = [ (d[i - 1][j - 1] + cost, (i - 1, j - 1, "match" if cost == 0 else "substitute")), (d[i - 1][j] + 1, (i - 1, j, "delete")), (d[i][j - 1] + 1, (i, j - 1, "insert")), ] if (i > 1 and j > 1 and a[i - 1] == b[j - 2] and a[i - 2] == b[j - 1]): cands.append((d[i - 2][j - 2] + 1, (i - 2, j - 2, "transpose"))) best = min(cands, key=lambda t: t[0]) d[i][j], pred[i][j] = best[0], best[1] # 回溯 i, j, ops = la, lb, [] while i > 0 or j > 0: pi, pj, kind = pred[i][j] if kind != "match": ops.append(kind) i, j = pi, pj ops = list(reversed(ops)) kinds = set(ops) if not kinds: t = "identical" elif len(ops) == 1: t = ops[0] elif len(ops) == 2 and kinds == {"transpose"}: t = "transpose" else: t = "+".join(sorted(kinds)) return d[la][lb], t def classify(a: str, b: str) -> dict: dist, kind = osa(a, b) same_ms = "".join(sorted(a)) == "".join(sorted(b)) if dist == 1 and kind == "transpose": gen = "Ⅳ 等长相邻换位" elif dist == 1 and kind == "substitute": gen = "Ⅲ 等长单字符替换" elif dist == 1 and kind in ("insert", "delete"): gen = "Ⅰ 长度错(增删一位)" elif dist == 0: gen = "同一串" else: gen = f"距离{dist}(非相邻变体)" return {"dist": dist, "edit": kind, "same_multiset": same_ms, "gen": gen} def diff_positions(a: str, b: str) -> list: return [i for i, (x, y) in enumerate(zip(a, b)) if x != y] # ---------- 主流程 ---------- def run(ids_path: str, use_net: bool, spill_dir: str = "spill") -> int: with open(ids_path, "r", encoding="utf-8") as f: ids = [ln.strip() for ln in f if ln.strip() and not ln.startswith("#")] cache = _load_cache() rows, resolvable = [], [] for pid in ids: sh = shape(pid) res = resolve(pid, cache, use_net) if sh["regex_ok"] else {"status": "SKIP(形状不合规,解析无意义)"} sh["resolve"] = res rows.append(sh) if sh["regex_ok"]: resolvable.append(pid) ok_refs = [r["raw"] for r in rows if r["resolve"].get("status") == "OK"] bad_ok = [r["raw"] for r in rows if r["resolve"].get("status") == "FAIL"] lines = [] lines.append("=== 形状层 ===") lines.append(f"{'len':>4} {'hexlen':>7} {'tail':>5} {'regex':>6} 判定 / 解析 / title") for r in rows: rs = r["resolve"] st = rs.get("status") extra = "" if st == "OK": extra = f"OK path={rs.get('path')} author={rs.get('author')} created={rs.get('created')} title={rs.get('title')}" else: extra = f"{st} {rs.get('message','')[:60]}" lines.append(f"{r['len']:>4} {r['hexlen']:>7} {r['tail']:>5} " f"{str(r['regex_ok']):>6} {r['gen_shape']} | {extra}") lines.append("") lines.append("=== 距离层(解析失败的合规串 × 参考集,仅列距离=1)===") if not bad_ok: lines.append("(无解析失败的合规串)") for bad in bad_ok: hits = 0 for ref in ok_refs: if ref == bad: continue c = classify(bad, ref) if c["dist"] == 1: hits += 1 pos = diff_positions(bad, ref) lines.append(f"- FAIL 串 …{bad[-12:]}") lines.append(f" ↔ 参考 …{ref[-12:]} dist=1 edit={c['edit']} " f"same_multiset={c['same_multiset']} ⇒ {c['gen']}") lines.append(f" 差异位置(0-based)={pos} " f"该位 FAIL={[bad[i] for i in pos]} REF={[ref[i] for i in pos]}") if c["edit"] == "transpose" and len(pos) == 2: lines.append(f" 换位细节:位置{pos} 上两侧字符互换") if hits == 0: lines.append(f"- FAIL 串 …{bad[-12:]}:参考集内无距离=1 的候选 ⇒ 判 Ⅴ(悬空)" f" 或参考集不全,二者同形,不可再分") lines.append("") lines.append("=== 边界 ===") lines.append("解析 OK ≠ 抄对了:必须把 title / author / created 与预期对表。") lines.append("解析 FAIL 时 Ⅴ(真的悬空)与「抄错成不存在的串」症状完全相同,只有回读能判,且判不了谁是谁。") lines.append("第 4 列 regex 通过只代表 Ⅰ/Ⅱ 未命中,不代表标识符正确。") lines.append(f"生成时间 {datetime.now(CST).strftime('%Y-%m-%d %H:%M:%S%z')} · resolve={'on' if use_net else 'off'}") out = "\n".join(lines) os.makedirs(spill_dir, exist_ok=True) # 命名带微秒并拒绝覆盖:同秒两次运行曾互相盖掉,等价于静默毁证。 ts = datetime.now(CST).strftime("%Y%m%dT%H%M%S_%f") path = os.path.join(spill_dir, f"{ts}-check_ids.txt") n = 2 while os.path.exists(path): path = os.path.join(spill_dir, f"{ts}_{n}-check_ids.txt") n += 1 with open(path, "x", encoding="utf-8") as f: # "x" 独占创建:已存在即报错,不覆盖 f.write(out + "\n") print(out) print() print(f"[spill] {path}") print(f"[spill] lines={len(out.splitlines())} bytes={os.path.getsize(path)}") return 0 def selftest() -> int: """今晚这起案件的三项断言。""" BADTXT = "441d9714a80eecec9fc379628a7d8c689d4dbcd9bd0d2ae9c2f96ee1fc03c14ei0" # 测枢文内串 GOODTX = "441d9714a80eecec9fc379628a7d8c689d4dbcd9bd0d2ae9c2f96ee1fc03a14ei0" # 鸡仔真件 cache = _load_cache() s_bad, s_good = shape(BADTXT), shape(GOODTX) r_bad = resolve(BADTXT, cache, True) r_good = resolve(GOODTX, cache, True) c = classify(BADTXT, GOODTX) pos = diff_positions(BADTXT, GOODTX) print("=== SELFTEST 断言 ===") a1 = s_bad["regex_ok"] and s_good["regex_ok"] print(f"[断言1] 两串 Ⅰ/Ⅱ 全过(形状门禁拦不住):{a1}") print(f" 测枢串 len={s_bad['len']} hexlen={s_bad['hexlen']} tail={s_bad['tail']} regex={s_bad['regex_ok']} 判定={s_bad['gen_shape']}") print(f" 鸡仔串 len={s_good['len']} hexlen={s_good['hexlen']} tail={s_good['tail']} regex={s_good['regex_ok']} 判定={s_good['gen_shape']}") a2 = r_bad.get("status") == "FAIL" and r_good.get("status") == "OK" print(f"[断言2] 测枢串解析失败 / 鸡仔串解析成功:{a2}") print(f" 测枢串 → {r_bad.get('status')} http={r_bad.get('http')} msg={r_bad.get('message','')[:40]}") print(f" 鸡仔串 → {r_good.get('status')} path={r_good.get('path')} created={r_good.get('created')}") print(f" 鸡仔串 title = {r_good.get('title')}") a3 = c["dist"] == 1 and c["edit"] == "substitute" and c["same_multiset"] is False print(f"[断言3] 两串 Damerau 距离=1 且编辑类型=单位替换(Ⅲ 代):{a3}") print(f" dist={c['dist']} edit={c['edit']} same_multiset={c['same_multiset']} 归类={c['gen']}") print(f" 差异位置(0-based)={pos} 测枢={[BADTXT[i] for i in pos]} 鸡仔={[GOODTX[i] for i in pos]}") allok = a1 and a2 and a3 print(f"\n=== SELFTEST 结果:{'三项全过' if allok else '未全过'} ===") return 0 if allok else 1 if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("--input") p.add_argument("--no-resolve", action="store_true") p.add_argument("--selftest", action="store_true") a = p.parse_args() if a.selftest: sys.exit(selftest()) if not a.input: print("need --input or --selftest", file=sys.stderr) sys.exit(2) sys.exit(run(a.input, not a.no_resolve))